-
Notifications
You must be signed in to change notification settings - Fork 137
Port Azure API-version settings from 0.61.0 #558
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { useEffect, useMemo, useState } from "react"; | ||
| import { | ||
| getProviderAzureApiVersion, | ||
| setProviderAzureApiVersion, | ||
| } from "../../../../lib/tauri"; | ||
|
|
||
| interface Props { | ||
| providerId: string; | ||
| disabled: boolean; | ||
| onChanged: () => void; | ||
| } | ||
|
|
||
| const BUILT_IN_OPTIONS = [ | ||
| { value: "", label: "Default" }, | ||
| { value: "v1", label: "OpenAI-compatible v1" }, | ||
| ]; | ||
|
|
||
| export function AzureApiVersionSection({ | ||
| providerId, | ||
| disabled, | ||
| onChanged, | ||
| }: Props) { | ||
| const [value, setValue] = useState<string>(""); | ||
| const [busy, setBusy] = useState(false); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| let stale = false; | ||
| setError(null); | ||
| void getProviderAzureApiVersion(providerId) | ||
| .then((next) => { | ||
| if (!stale) setValue(next ?? ""); | ||
| }) | ||
| .catch((reason: unknown) => { | ||
| if (!stale) setError(String(reason)); | ||
| }); | ||
| return () => { | ||
| stale = true; | ||
| }; | ||
| }, [providerId]); | ||
|
|
||
| const options = useMemo(() => { | ||
| if (!value || BUILT_IN_OPTIONS.some((option) => option.value === value)) { | ||
| return BUILT_IN_OPTIONS; | ||
| } | ||
| return [...BUILT_IN_OPTIONS, { value, label: value }]; | ||
|
Comment on lines
+42
to
+46
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,140p' apps/desktop-tauri/src/surfaces/settings/providers/sections/AzureApiVersionSection.tsx
rg -n 'Custom.*[Aa]pi|apiVersion|API version|AzureApiVersionSection' apps/desktop-tauri/src/surfaces/settings/providersRepository: nesszer/Win-CodexBar Length of output: 3095 🏁 Script executed: set -eu
printf '%s\n' '--- bridge references ---'
rg -n -C 5 'setProviderAzureApiVersion|getProviderAzureApiVersion|azure_api_version|AzureApiVersion' apps/desktop-tauri/src
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(tauri|lib|settings|provider).*(rs|ts|tsx)$|AzureApiVersion'Repository: nesszer/Win-CodexBar Length of output: 22868 🏁 Script executed: set -eu
printf '%s\n' '--- Tauri command definitions ---'
rg -n -C 8 'set_provider_azure_api_version|get_provider_azure_api_version|azure_api_version' apps/desktop-tauri/src-tauri rust
printf '%s\n' '--- configured version consumers ---'
rg -n -C 6 'AZURE_OPENAI_API_VERSION|api_version|api-version|apiVersion' rust/src/providers/azureopenai.rs rust/src/settings apps/desktop-tauri/src-tauriRepository: nesszer/Win-CodexBar Length of output: 32794 Add a control for a new custom API version.
🤖 Prompt for AI Agents |
||
| }, [value]); | ||
|
|
||
| const handleChange = async (next: string) => { | ||
| if (next === value || busy || disabled) return; | ||
| setBusy(true); | ||
| setError(null); | ||
| try { | ||
| await setProviderAzureApiVersion(providerId, next); | ||
| setValue(next); | ||
| onChanged(); | ||
| } catch (reason: unknown) { | ||
| setError(String(reason)); | ||
| } finally { | ||
| setBusy(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <section className="provider-detail-section provider-detail-region"> | ||
| <h4>Azure OpenAI API version</h4> | ||
| <select | ||
| className="provider-detail-select" | ||
| value={value} | ||
| disabled={disabled || busy} | ||
| aria-label="Azure OpenAI API version" | ||
| onChange={(event) => void handleChange(event.target.value)} | ||
| > | ||
| {options.map((option) => ( | ||
| <option key={option.value} value={option.value}> | ||
| {option.label} | ||
| </option> | ||
| ))} | ||
| </select> | ||
| <p className="provider-detail-helper"> | ||
| Default uses AZURE_OPENAI_API_VERSION, then 2024-10-21. | ||
| </p> | ||
| {error && <p className="provider-detail-error">{error}</p>} | ||
| </section> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,17 +102,51 @@ impl AzureOpenAIProvider { | |
|
|
||
| fn resolve_config(ctx: &FetchContext) -> Result<AzureOpenAIConfig, ProviderError> { | ||
| if let Some(raw) = ctx.api_key.as_deref().and_then(clean_string) { | ||
| return Self::parse_saved_config(&raw); | ||
| let config = Self::parse_saved_config(&raw)?; | ||
| return Ok(Self::apply_saved_api_version( | ||
| config, | ||
| &raw, | ||
| ApiKeys::load().api_version("azureopenai"), | ||
| )); | ||
| } | ||
| if let Some(config) = Self::config_from_env()? { | ||
| return Ok(config); | ||
| } | ||
| if let Some(raw) = ApiKeys::load().get("azureopenai") { | ||
| return Self::parse_saved_config(raw); | ||
| let config = Self::parse_saved_config(raw)?; | ||
| return Ok(Self::apply_saved_api_version( | ||
| config, | ||
| raw, | ||
| ApiKeys::load().api_version("azureopenai"), | ||
| )); | ||
| } | ||
| Err(ProviderError::AuthRequired) | ||
| } | ||
|
|
||
| fn apply_saved_api_version( | ||
| mut config: AzureOpenAIConfig, | ||
| raw: &str, | ||
| stored_api_version: Option<&str>, | ||
| ) -> AzureOpenAIConfig { | ||
| if !Self::has_explicit_api_version(raw) | ||
| && let Some(api_version) = stored_api_version.and_then(clean_string) | ||
| { | ||
| config.api_version = api_version; | ||
| } | ||
| config | ||
| } | ||
|
|
||
| fn has_explicit_api_version(raw: &str) -> bool { | ||
| if let Ok(value) = serde_json::from_str::<serde_json::Value>(raw) { | ||
| return value | ||
| .get("api_version") | ||
| .and_then(serde_json::Value::as_str) | ||
| .and_then(clean_string) | ||
| .is_some(); | ||
| } | ||
| raw.split('|').nth(3).and_then(clean_string).is_some() | ||
| } | ||
|
|
||
| fn config_from_env() -> Result<Option<AzureOpenAIConfig>, ProviderError> { | ||
| let Some(api_key) = clean_env("AZURE_OPENAI_API_KEY") else { | ||
| return Ok(None); | ||
|
Comment on lines
102
to
152
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,190p' rust/src/providers/azureopenai.rs
sed -n '360,440p' rust/src/providers/azureopenai.rs
rg -n 'parse_saved_config|has_explicit_api_version|api_version|AzureOpenAI' rust/src/providers/azureopenai.rs rust/src/settings/api_keys.rsRepository: nesszer/Win-CodexBar Length of output: 17579 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- parser and URL logic ---'
sed -n '165,285p' rust/src/providers/azureopenai.rs
printf '%s\n' '--- provider tests and registration ---'
sed -n '320,455p' rust/src/providers/azureopenai.rs
printf '%s\n' '--- ApiKeys Azure entry and accessors ---'
sed -n '1,110p' rust/src/settings/api_keys.rs
sed -n '230,270p' rust/src/settings/api_keys.rs
printf '%s\n' '--- Azure references outside the provider ---'
rg -n -C 3 'AZURE_OPENAI_|azureopenai|api_version|api-version' --glob '!target/**' --glob '!node_modules/**' .Repository: nesszer/Win-CodexBar Length of output: 46234 Honor 🤖 Prompt for AI Agents |
||
|
|
@@ -357,6 +391,27 @@ mod tests { | |
| assert_eq!(config.api_version, "v1"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn stored_api_version_overrides_missing_composite_version() { | ||
| let config = | ||
| AzureOpenAIProvider::parse_saved_config("key|example.openai.azure.com|chat-prod") | ||
| .unwrap(); | ||
| let config = AzureOpenAIProvider::apply_saved_api_version( | ||
| config, | ||
| "key|example.openai.azure.com|chat-prod", | ||
| Some("v1"), | ||
| ); | ||
| assert_eq!(config.api_version, "v1"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn explicit_saved_api_version_wins_over_picker_value() { | ||
| let raw = r#"{"api_key":"key","endpoint":"example.openai.azure.com","deployment":"chat-prod","api_version":"2024-10-21"}"#; | ||
| let config = AzureOpenAIProvider::parse_saved_config(raw).unwrap(); | ||
| let config = AzureOpenAIProvider::apply_saved_api_version(config, raw, Some("v1")); | ||
| assert_eq!(config.api_version, "2024-10-21"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn rejects_insecure_or_tricky_endpoint_overrides() { | ||
| assert!(AzureOpenAIProvider::parse_endpoint("http://example.com").is_err()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,10 @@ pub struct ApiKeyEntry { | |
| /// Optional label for the key (e.g., "Personal", "Work") | ||
| #[serde(default)] | ||
| pub label: Option<String>, | ||
| /// Azure OpenAI API-version override kept alongside the credential. | ||
| /// `None` inherits `AZURE_OPENAI_API_VERSION` and the provider default. | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub api_version: Option<String>, | ||
| } | ||
|
|
||
| impl ApiKeys { | ||
|
|
@@ -57,16 +61,39 @@ impl ApiKeys { | |
| /// Set API key for a provider | ||
| pub fn set(&mut self, provider_id: &str, api_key: &str, label: Option<&str>) { | ||
| let now = chrono::Utc::now().format("%Y-%m-%d %H:%M").to_string(); | ||
| let api_version = self | ||
| .keys | ||
| .get(provider_id) | ||
| .and_then(|entry| entry.api_version.clone()); | ||
| self.keys.insert( | ||
| provider_id.to_string(), | ||
| ApiKeyEntry { | ||
| api_key: api_key.to_string(), | ||
| saved_at: now, | ||
| label: label.map(|s| s.to_string()), | ||
| api_version, | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| /// Get a provider-specific API-version override, if one is stored. | ||
| pub fn api_version(&self, provider_id: &str) -> Option<&str> { | ||
| self.keys | ||
| .get(provider_id) | ||
| .and_then(|entry| entry.api_version.as_deref()) | ||
| .map(str::trim) | ||
| .filter(|value| !value.is_empty()) | ||
| } | ||
|
|
||
| /// Store or clear a provider-specific API-version override. | ||
| pub fn set_api_version(&mut self, provider_id: &str, api_version: Option<String>) { | ||
| if let Some(entry) = self.keys.get_mut(provider_id) { | ||
| entry.api_version = api_version | ||
| .map(|value| value.trim().to_string()) | ||
| .filter(|value| !value.is_empty()); | ||
| } | ||
|
Comment on lines
+90
to
+94
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,125p' rust/src/settings/api_keys.rs
sed -n '145,210p' apps/desktop-tauri/src-tauri/src/commands/provider_settings.rs
sed -n '1,120p' apps/desktop-tauri/src/surfaces/settings/providers/sections/AzureApiVersionSection.tsx
rg -n 'set_api_version|api_version\\(' rust apps/desktop-tauri/src-tauri/srcRepository: nesszer/Win-CodexBar Length of output: 9227 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- Azure section callers and provider-settings UI ---'
rg -n -C 8 'AzureApiVersionSection|azure.*api.version|apiVersion|api_version' apps/desktop-tauri/src rust/src -g '*.tsx' -g '*.ts' -g '*.rs' | head -n 500
printf '%s\n' '--- ApiKeys mutation and credential-entry creation ---'
rg -n -C 8 'ApiKeys::(load|set|remove)|keys\.set\(|\.set\([^;]*api_key|api_keys|api key|credential' apps/desktop-tauri/src-tauri/src rust/src -g '*.rs' | head -n 700
printf '%s\n' '--- resolver/provider use ---'
rg -n -C 8 'api_version\(|AZURE_OPENAI_API_VERSION|AzureOpenAI' rust/src apps/desktop-tauri/src-tauri/src -g '*.rs' | head -n 700Repository: nesszer/Win-CodexBar Length of output: 50377 🤖 get_repo_knowledge executed:
Length of output: 46188 Reject an API-version override when no credential entry exists. The Azure API-version picker is available before an Azure credential entry exists. Return an error until an Azure OpenAI credential entry exists. Do not store the override independently unless the resolver also reads that separate setting. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /// Remove API key for a provider | ||
| pub fn remove(&mut self, provider_id: &str) { | ||
| self.keys.remove(provider_id); | ||
|
|
@@ -579,3 +606,29 @@ pub fn get_api_key_providers() -> Vec<ProviderConfigInfo> { | |
| }, | ||
| ] | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::ApiKeys; | ||
|
|
||
| #[test] | ||
| fn api_version_survives_api_key_update() { | ||
| let mut keys = ApiKeys::default(); | ||
| keys.set("azureopenai", "key", Some("work")); | ||
| keys.set_api_version("azureopenai", Some("v1".to_string())); | ||
| keys.set("azureopenai", "new-key", None); | ||
|
|
||
| assert_eq!(keys.get("azureopenai"), Some("new-key")); | ||
| assert_eq!(keys.api_version("azureopenai"), Some("v1")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn clearing_api_version_removes_the_override() { | ||
| let mut keys = ApiKeys::default(); | ||
| keys.set("azureopenai", "key", None); | ||
| keys.set_api_version("azureopenai", Some("2025-01-01".to_string())); | ||
| keys.set_api_version("azureopenai", None); | ||
|
|
||
| assert_eq!(keys.api_version("azureopenai"), None); | ||
| } | ||
| } | ||
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.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,140p' apps/desktop-tauri/src/surfaces/settings/providers/sections/AzureApiVersionSection.tsxRepository: nesszer/Win-CodexBar
Length of output: 2435
🏁 Script executed:
Repository: nesszer/Win-CodexBar
Length of output: 12944
🤖 get_repo_knowledge executed:
get_repo_knowledge nesszer/Win-CodexBar /tmp/coderabbit-repo-knowledge/nesszer-win-codexbar-c18ba9e7/architectureLength of output: 46207
🏁 Script executed:
Repository: nesszer/Win-CodexBar
Length of output: 9989
Invalidate the pending read when saving a new value.
busydisables only the select. It does not invalidategetProviderAzureApiVersion. If the getter resolves or rejects aftersetProviderAzureApiVersionsucceeds, itsstaleflag is still false, so it can overwrite the saved value or display an error. Track a request generation or invalidate the pending read before saving.🤖 Prompt for AI Agents