-
Notifications
You must be signed in to change notification settings - Fork 31
fix(azure): route gpt-5.x reasoning_effort via the v1 API (api-version=preview) #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,6 +58,29 @@ impl AzureProvider { | |
| fn api_version(&self) -> String { | ||
| self.config.params.get("api_version").unwrap().clone() | ||
| } | ||
|
|
||
| /// Whether this provider targets Azure's next-generation v1 API surface | ||
| /// (`/openai/v1/...`), selected by setting `api_version` to "preview" or "v1". | ||
| /// The v1 API is required for the latest models and parameters (for example | ||
| /// `reasoning_effort` on gpt-5.x); dated api-versions keep the legacy | ||
| /// deployment-in-path form. | ||
| fn uses_v1_api(&self) -> bool { | ||
| matches!(self.api_version().as_str(), "preview" | "v1") | ||
| } | ||
|
|
||
| /// Base URL for the v1 API. When `base_url` is configured it is used verbatim | ||
| /// (operators point it at the `/openai/v1` root); otherwise it is built from | ||
| /// `resource_name`. | ||
| fn v1_base(&self) -> String { | ||
| if let Some(base_url) = self.config.params.get("base_url") { | ||
| base_url.trim_end_matches('/').to_string() | ||
| } else { | ||
| format!( | ||
| "https://{}.openai.azure.com/openai/v1", | ||
| self.config.params.get("resource_name").unwrap(), | ||
| ) | ||
| } | ||
| } | ||
|
Comment on lines
+71
to
+83
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Read-only verification: find Azure preview/v1 configs that might still use legacy-style base URLs.
# Expected: preview/v1 configs should point to /openai/v1 (or be empty so resource_name-derived URL is used).
rg -n -C2 '"api_version"\s*:\s*"(preview|v1)"'
rg -n -C2 '"base_url"\s*:\s*"'Repository: traceloop/hub Length of output: 39 🏁 Script executed: # Find where v1_base() is called and how the result is used
rg -n "v1_base\(\)" src/providers/azure/provider.rs
# Check if base_url is validated during config construction/parsing
rg -n "base_url" src/providers/azure/provider.rs -A 2 -B 2
# Search for test coverage of base_url handling
rg -n "base_url" --type rust | grep -i testRepository: traceloop/hub Length of output: 4532 🏁 Script executed: # Find uses_v1_api() definition to understand when v1 mode is triggered
rg -n "fn uses_v1_api" src/providers/azure/provider.rs -A 5
# Check api_version extraction/parsing and validation
rg -n "api_version" src/providers/azure/provider.rs -B 2 -A 2
# Search for any config validation that checks base_url format
rg -n "validate\|check\|ensure" src/providers/azure/ -i
# Look at the broader Azure config structure
fd "azure" --type f src/ | xargs rg "struct.*Config" -lRepository: traceloop/hub Length of output: 3312 🏁 Script executed: # Check for config validation logic or documentation about base_url format
rg -n "base_url" src/config/ -A 3 -B 3
# Check if there's any documentation or comments about base_url configuration for Azure
rg -n "base_url" --type md
# Look for where base_url gets validated or checked in the provider chain
rg -n "transform_provider_dto\|AzureProviderConfig" src/ -A 10 -B 2 | head -80Repository: traceloop/hub Length of output: 192 Document base_url format requirement for v1 API mode. When using 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| #[async_trait] | ||
|
|
@@ -113,15 +136,27 @@ impl Provider for AzureProvider { | |
|
|
||
| let deployment = model_config.params.get("deployment").unwrap(); | ||
| let api_version = self.api_version(); | ||
| let url = format!( | ||
| "{}/{}/chat/completions?api-version={}", | ||
| self.endpoint(), | ||
| deployment, | ||
| api_version | ||
| ); | ||
|
|
||
| // Convert to Azure-specific request format | ||
| let azure_request = AzureChatCompletionRequest::from(payload.clone()); | ||
| let mut azure_request = AzureChatCompletionRequest::from(payload.clone()); | ||
|
|
||
| // The v1 API routes by the `model` field in the body rather than a deployment | ||
| // in the URL path. Legacy dated api-versions keep the deployment-in-path form. | ||
| let url = if self.uses_v1_api() { | ||
| azure_request.base.model = deployment.clone(); | ||
| format!( | ||
| "{}/chat/completions?api-version={}", | ||
| self.v1_base(), | ||
| api_version | ||
| ) | ||
| } else { | ||
| format!( | ||
| "{}/{}/chat/completions?api-version={}", | ||
| self.endpoint(), | ||
| deployment, | ||
| api_version | ||
| ) | ||
| }; | ||
|
|
||
| let response = self | ||
| .http_client | ||
|
|
@@ -209,12 +244,20 @@ impl Provider for AzureProvider { | |
| let deployment = model_config.params.get("deployment").unwrap(); | ||
| let api_version = self.api_version(); | ||
|
|
||
| let url = format!( | ||
| "{}/{}/embeddings?api-version={}", | ||
| self.endpoint(), | ||
| deployment, | ||
| api_version | ||
| ); | ||
| // The v1 API routes by the `model` field in the body rather than a deployment | ||
| // in the URL path. Legacy dated api-versions keep the deployment-in-path form. | ||
| let mut payload = payload; | ||
| let url = if self.uses_v1_api() { | ||
| payload.model = deployment.clone(); | ||
| format!("{}/embeddings?api-version={}", self.v1_base(), api_version) | ||
| } else { | ||
| format!( | ||
| "{}/{}/embeddings?api-version={}", | ||
| self.endpoint(), | ||
| deployment, | ||
| api_version | ||
| ) | ||
| }; | ||
|
|
||
| let response = self | ||
| .http_client | ||
|
|
@@ -244,6 +287,44 @@ impl Provider for AzureProvider { | |
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod v1_routing_tests { | ||
| use super::*; | ||
| use std::collections::HashMap; | ||
|
|
||
| fn provider(params: &[(&str, &str)]) -> AzureProvider { | ||
| AzureProvider { | ||
| config: ProviderConfig { | ||
| key: "azure".to_string(), | ||
| r#type: ProviderType::Azure, | ||
| api_key: "test-key".to_string(), | ||
| params: params | ||
| .iter() | ||
| .map(|(k, v)| (k.to_string(), v.to_string())) | ||
| .collect::<HashMap<_, _>>(), | ||
| }, | ||
| http_client: Client::new(), | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn preview_api_version_selects_v1_base() { | ||
| let p = provider(&[("resource_name", "my-res"), ("api_version", "preview")]); | ||
| assert!(p.uses_v1_api()); | ||
| assert_eq!(p.v1_base(), "https://my-res.openai.azure.com/openai/v1"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn dated_api_version_keeps_legacy_deployment_path() { | ||
| let p = provider(&[("resource_name", "my-res"), ("api_version", "2024-10-21")]); | ||
| assert!(!p.uses_v1_api()); | ||
| assert_eq!( | ||
| p.endpoint(), | ||
| "https://my-res.openai.azure.com/openai/deployments" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod reasoning_effort_precedence_tests { | ||
| use super::*; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Expanded accepted effort set is not propagated to non-OpenAI conversion path.
After this change,
validate()acceptsminimalandxhigh, butto_thinking_prompt()(Line 81 onward) still only handleslow|medium|high, so newly accepted values are silently treated as no prompt (None). This creates a behavior gap for Anthropic/Bedrock-style paths.Suggested fix
pub fn to_thinking_prompt(&self) -> Option<String> { if self.max_tokens.is_some() { // If max_tokens is specified, use a generic thinking prompt Some("Think through this step-by-step with detailed reasoning.".to_string()) } else { match self.effort.as_deref() { Some(effort) if !effort.trim().is_empty() => match effort { - "high" => { + "high" | "xhigh" => { Some("Think through this step-by-step with detailed reasoning.".to_string()) } "medium" => Some("Consider this problem thoughtfully.".to_string()), - "low" => Some("Think about this briefly.".to_string()), + "low" | "minimal" => Some("Think about this briefly.".to_string()), + "none" => None, _ => None, }, _ => None, } } }Also applies to: 43-43
🤖 Prompt for AI Agents