feat(server): realtime handle & migration (#15487)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Retry failed Copilot transcription tasks in real time.
  * Retried tasks resume processing and report updated status.
* Managed Copilot provider models can be omitted to use provider
defaults.

* **Bug Fixes**
* Improved handling of incomplete BYOK profiles, including safe
replacement of legacy records.
  * Duplicate profile creation now returns a clear validation error.
* Improved transcript processing reliability by preventing duplicate or
stale dispatches.

* **Migration**
* Consolidated legacy managed-provider settings while preserving
existing profiles and defaults.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-08-16 08:04:19 +08:00
committed by GitHub
parent ff1e3d9c94
commit 06b3d020fa
27 changed files with 1118 additions and 175 deletions
@@ -103,6 +103,21 @@ pub(in super::super) async fn create(
definition, sort_order, enabled, created_by, updated_by, created_at, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (workspace_id, provider, name) DO UPDATE
SET id = EXCLUDED.id,
description = EXCLUDED.description,
encrypted_api_key = EXCLUDED.encrypted_api_key,
definition = EXCLUDED.definition,
sort_order = EXCLUDED.sort_order,
enabled = EXCLUDED.enabled,
revision = 1,
credential_generation = 1,
validation = NULL,
created_by = EXCLUDED.created_by,
updated_by = EXCLUDED.updated_by,
created_at = EXCLUDED.created_at,
updated_at = EXCLUDED.updated_at
WHERE ai_workspace_byok_configs.definition = '{}'::jsonb
RETURNING id, workspace_id, provider, name, description, encrypted_api_key,
definition, sort_order, enabled, revision, credential_generation, validation
"#,
@@ -117,9 +132,10 @@ pub(in super::super) async fn create(
.bind(sort_order)
.bind(input.enabled)
.bind(&input.actor_user_id)
.fetch_one(&mut *tx)
.fetch_optional(&mut *tx)
.await
.map_err(|error| RuntimeError::database("create BYOK profile failed", error))?;
.map_err(|error| RuntimeError::database("create BYOK profile failed", error))?
.ok_or_else(|| RuntimeError::invalid_input("BYOK profile name already exists"))?;
tx.commit()
.await
.map_err(|error| RuntimeError::database("create BYOK profile commit failed", error))?;
@@ -578,7 +594,14 @@ pub(super) fn require_text(value: &str, field: &'static str) -> RuntimeResult<()
#[cfg(test)]
mod tests {
use super::{PgPool, Uuid, list};
use super::{ByokPolicy, PgPool, Uuid, create, list};
use crate::{
llm::{
ByokCapabilityInput, ByokEndpointInput, ByokModelDeclarationInput, ByokProfileDefinitionInput,
CreateByokProfileInput, Deployment,
},
runtime::config::CopilotByokRuntimeConfig,
};
#[tokio::test]
async fn list_skips_rows_with_unparseable_legacy_definition() {
@@ -619,6 +642,40 @@ mod tests {
assert_eq!(profiles.len(), 1);
assert_eq!(profiles[0].name, "valid");
let input = || CreateByokProfileInput {
workspace_id: workspace_id.clone(),
provider: "openai".to_string(),
name: "legacy".to_string(),
description: None,
credential: "replacement-key".to_string(),
definition: ByokProfileDefinitionInput {
endpoint: ByokEndpointInput {
kind: "provider_default".to_string(),
url: None,
dialect: None,
},
models: vec![ByokModelDeclarationInput {
model_id: "gpt-4o-mini".to_string(),
enabled: true,
capabilities: vec![ByokCapabilityInput {
input: vec!["text".to_string()],
output: vec!["text".to_string()],
features: vec![],
attachment_kinds: vec![],
attachment_sources: vec![],
}],
}],
},
enabled: true,
actor_user_id: "user-1".to_string(),
};
let policy = ByokPolicy::from(Deployment::Cloud, &CopilotByokRuntimeConfig::default());
create(&pool, &[7; 32], &policy, input()).await.unwrap();
let profiles = list(&pool, &workspace_id).await.unwrap();
assert_eq!(profiles.len(), 2);
assert!(profiles.iter().any(|profile| profile.name == "legacy"));
assert!(create(&pool, &[7; 32], &policy, input()).await.is_err());
sqlx::query("DELETE FROM ai_workspace_byok_configs WHERE workspace_id = $1")
.bind(&workspace_id)
.execute(&pool)
+54 -2
View File
@@ -158,7 +158,7 @@ pub(crate) struct CopilotManagedProfileConfigFile {
priority: Option<f64>,
#[serde(default = "enabled_by_default")]
enabled: bool,
models: Vec<String>,
models: Option<Vec<String>>,
middleware: Option<CopilotProviderMiddlewareConfigFile>,
config: Map<String, serde_json::Value>,
}
@@ -193,6 +193,18 @@ impl CopilotManagedProvider {
Self::OpenAi => "openai",
}
}
fn legacy_models(self) -> Vec<String> {
let models: &[&str] = match self {
Self::OpenAi => &["gpt-5.6-luna", "gpt-5.6-terra", "gpt-image-1", "gpt-4o-mini"],
Self::CloudflareWorkersAi => &["@cf/baai/bge-reranker-base"],
Self::Fal => &["lora/image-to-image", "workflowutils/teed"],
Self::Gemini => &["gemini-3.7-flash", "gemini-embedding-001"],
Self::GeminiVertex => &["gemini-3.7-flash"],
Self::Anthropic | Self::AnthropicVertex => &["claude-sonnet-4-6"],
};
models.iter().map(|model| (*model).to_string()).collect()
}
}
#[derive(Clone, Deserialize, serde::Serialize, schemars::JsonSchema)]
@@ -270,11 +282,12 @@ impl TryFrom<CopilotManagedProfileConfigFile> for CopilotManagedProfileConfig {
"managed copilot profile id must contain only letters, numbers, hyphens, and underscores",
));
}
let models = value.models.unwrap_or_else(|| value.provider.legacy_models());
Ok(Self {
id: value.id,
provider: value.provider.as_str().to_string(),
enabled: value.enabled,
models: value.models,
models,
config: serde_json::Value::Object(value.config),
})
}
@@ -740,6 +753,45 @@ mod tests {
assert_eq!(copilot.providers.profiles.len(), 1);
assert_eq!(copilot.providers.profiles[0].id, "managed-openai");
for (provider, expected_models) in [
(
"openai",
vec!["gpt-5.6-luna", "gpt-5.6-terra", "gpt-image-1", "gpt-4o-mini"],
),
("cloudflareWorkersAi", vec!["@cf/baai/bge-reranker-base"]),
("fal", vec!["lora/image-to-image", "workflowutils/teed"]),
("gemini", vec!["gemini-3.7-flash", "gemini-embedding-001"]),
("geminiVertex", vec!["gemini-3.7-flash"]),
("anthropic", vec!["claude-sonnet-4-6"]),
("anthropicVertex", vec!["claude-sonnet-4-6"]),
] {
let app_config = app_config_from_flat_overrides([(
"copilot.providers.profiles",
serde_json::json!([{
"id": format!("{provider}-default"),
"type": provider,
"config": {}
}]),
)])
.unwrap();
let copilot: CopilotRuntimeConfig = app_config.copilot.unwrap().try_into().unwrap();
validate_copilot_config(&copilot).unwrap();
assert_eq!(copilot.providers.profiles[0].models, expected_models);
}
let app_config = app_config_from_flat_overrides([(
"copilot.providers.profiles",
serde_json::json!([{
"id": "managed-openai",
"type": "openai",
"models": [],
"config": {}
}]),
)])
.unwrap();
let copilot: CopilotRuntimeConfig = app_config.copilot.unwrap().try_into().unwrap();
assert!(validate_copilot_config(&copilot).is_err());
let directory = tempfile::tempdir().unwrap();
let base_path = directory.path().join("base.json");
let override_path = directory.path().join("override.json");