| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| use maka_agent_facts::configuration::ApiProtocol; | |
| use maka_agent_facts::configuration::ModelInfo; | |
| use maka_agent_facts::configuration::ModelOverride; | |
| use maka_agent_facts::execution::ThinkingLevel::High; | |
| use maka_agent_facts::execution::ThinkingLevel::Low; | |
| use maka_agent_facts::execution::ThinkingLevel::Max; | |
| use maka_agent_facts::execution::ThinkingLevel::Medium; | |
| use maka_agent_facts::execution::ThinkingLevel::Off; | |
| use maka_agent_facts::execution::ThinkingLevel::Xhigh; | |
| use maka_agent_facts::model::request::ProviderKind; | |
| use maka_agent_ports::provider::Connection; | |
| use maka_agent_ports::provider::Definition; | |
| use maka_agent_ports::provider::Resolve; | |
| use maka_providers::api::ApiProviders; | |
| use serde_json::json; | |
| #[path = "api/discovery.rs"] | |
| mod discovery; | |
| #[path = "api/effects.rs"] | |
| mod effects; | |
| #[path = "api/policy.rs"] | |
| mod policy; | |
| fn snapshot() -> std::sync::Arc<maka_providers::catalog::Snapshot> { | |
| std::sync::Arc::new(maka_providers::catalog::Snapshot::bundled().unwrap()) | |
| } | |
| fn binding(name: &str) -> Definition { | |
| ApiProviders::definition(name, snapshot()).unwrap() | |
| } | |
| fn request(binding: &Definition, id: &str, overrides: ModelOverride) -> Resolve { | |
| let configuration = binding | |
| .configure(json!({"baseUrl":"https://relay.test/v1"})) | |
| .unwrap(); | |
| Resolve { | |
| connection: Connection { | |
| id: "test-account".into(), | |
| revision: 1, | |
| configuration, | |
| }, | |
| model: ModelInfo::new(id), | |
| overrides: Some(overrides), | |
| thinking_level: None, | |
| } | |
| } | |
| #[tokio::test] | |
| async fn public_provider_binding_preserves_protocol_contracts_and_explicit_policy() { | |
| for name in ["ollama", "localai"] { | |
| let binding = binding(name); | |
| assert!(binding.descriptor().anonymous); | |
| let connection = request(&binding, "custom", ModelOverride::default()).connection; | |
| let credentials = binding | |
| .authorize(connection, None, "session".into()) | |
| .await | |
| .unwrap(); | |
| assert!( | |
| matches!(credentials, maka_agent_ports::model::Credentials::RequestHeaders(headers) if headers.is_empty()) | |
| ); | |
| } | |
| let required = binding("openai"); | |
| assert!(!required.descriptor().anonymous); | |
| assert!( | |
| required | |
| .authorize( | |
| request(&required, "custom", ModelOverride::default()).connection, | |
| None, | |
| "session".into() | |
| ) | |
| .await | |
| .is_err() | |
| ); | |
| for (name, summary) in [ | |
| ("deepseek", false), | |
| ("moonshot-global", true), | |
| ("alibaba-token-plan", true), | |
| ] { | |
| let binding = binding(name); | |
| let input = request( | |
| &binding, | |
| "custom", | |
| ModelOverride { | |
| api_protocol: Some(ApiProtocol::OpenaiResponses), | |
| thinking_levels: Some(vec![Off, High]), | |
| default_thinking_level: Some(High), | |
| ..Default::default() | |
| }, | |
| ); | |
| let model = binding.prepare(input.clone()).await.unwrap(); | |
| assert!(matches!(model.protocol, ProviderKind::OpenResponses(_))); | |
| assert_eq!(model.adapter, "responses"); | |
| assert_eq!( | |
| model.provider_options["openResponses"]["reasoningEffort"], | |
| "high" | |
| ); | |
| assert_eq!( | |
| model.provider_options["openResponses"] | |
| .get("reasoningSummary") | |
| .is_some(), | |
| summary | |
| ); | |
| let model = binding | |
| .prepare(Resolve { | |
| thinking_level: Some(Off), | |
| ..input | |
| }) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| model.provider_options, | |
| json!({"openResponses":{"reasoningEffort":"none"}}) | |
| ); | |
| } | |
| for (provider, expected_name) in [ | |
| ("openai-compatible", "test-account"), | |
| ("openrouter", "openrouter"), | |
| ] { | |
| let binding = binding(provider); | |
| let model = binding | |
| .prepare(request(&binding, "future-model", ModelOverride::default())) | |
| .await | |
| .unwrap(); | |
| assert!(matches!( | |
| model.protocol, | |
| ProviderKind::OpenaiCompatible { name } if name == expected_name | |
| )); | |
| } | |
| for (name, protocol, adapter) in [ | |
| ( | |
| "openai-compatible", | |
| ApiProtocol::OpenaiChatCompletions, | |
| "chat-completions", | |
| ), | |
| ( | |
| "openai-responses-compatible", | |
| ApiProtocol::OpenaiResponses, | |
| "responses", | |
| ), | |
| ( | |
| "anthropic-compatible", | |
| ApiProtocol::AnthropicMessages, | |
| "anthropic-messages", | |
| ), | |
| ] { | |
| let binding = binding(name); | |
| let mut input = request( | |
| &binding, | |
| "future-model", | |
| ModelOverride { | |
| api_protocol: Some(protocol), | |
| max_output_tokens: Some(5000), | |
| ..Default::default() | |
| }, | |
| ); | |
| input.model.max_output_tokens = Some(4000); | |
| let model = binding.prepare(input).await.unwrap(); | |
| assert_eq!(model.adapter, adapter); | |
| assert_eq!(model.main_output_limit, Some(4000)); | |
| assert_eq!(model.info.max_output_tokens, Some(4000)); | |
| } | |
| } | |
| #[tokio::test] | |
| async fn account_metadata_and_reply_capacity_survive_plugin_policy_resolution() { | |
| let anthropic = binding("anthropic"); | |
| let mut input = request( | |
| &anthropic, | |
| "claude-sonnet-4-5", | |
| ModelOverride { | |
| max_output_tokens: Some(5000), | |
| ..Default::default() | |
| }, | |
| ); | |
| input.model.max_output_tokens = Some(4000); | |
| let model = anthropic.prepare(input.clone()).await.unwrap(); | |
| assert_eq!( | |
| model.provider_options["anthropic"]["thinking"]["budgetTokens"], | |
| 1024 | |
| ); | |
| assert_eq!(model.main_output_limit, Some(2976)); | |
| input.overrides.as_mut().unwrap().max_output_tokens = Some(1000); | |
| assert_eq!( | |
| serde_json::to_value(anthropic.prepare(input).await.unwrap_err()).unwrap(), | |
| json!({"kind":"insufficient_output_budget"}) | |
| ); | |
| let openai = binding("openai"); | |
| for (id, label) in [("gpt-6-sol", "GPT-6 Sol"), ("gpt-6-luna", "GPT-6 Luna")] { | |
| let mut input = request(&openai, id, ModelOverride::default()); | |
| input.thinking_level = Some(Max); | |
| let model = openai.prepare(input).await.unwrap(); | |
| assert!(matches!(model.protocol, ProviderKind::OpenaiResponses)); | |
| assert_eq!(model.info.id, id); | |
| assert_eq!(model.info.display_name.as_deref(), Some(label)); | |
| assert_eq!(model.thinking_levels, [Low, Medium, High, Xhigh, Max]); | |
| assert_eq!(model.provider_options["openai"]["reasoningEffort"], "max"); | |
| } | |
| let input = request(&openai, "gpt-5.2", ModelOverride::default()); | |
| let model = openai.prepare(input.clone()).await.unwrap(); | |
| assert_eq!( | |
| model.provider_options, | |
| json!({"openai":{"store":false,"parallelToolCalls":true}}) | |
| ); | |
| let mut account = input; | |
| account.model.thinking_levels = Some(vec![High]); | |
| let model = openai.prepare(account).await.unwrap(); | |
| assert!( | |
| model.provider_options["openai"] | |
| .get("reasoningEffort") | |
| .is_none() | |
| ); | |
| let mut input = request( | |
| &openai, | |
| "gpt-5.2", | |
| ModelOverride { | |
| thinking_levels: Some(vec![Max]), | |
| default_thinking_level: Some(Max), | |
| ..Default::default() | |
| }, | |
| ); | |
| input.model.context_window = Some(12345); | |
| input.model.supports_reasoning_summary = Some(false); | |
| input.model.thinking_levels = Some(vec![]); | |
| let model = openai.prepare(input).await.unwrap(); | |
| assert_eq!(model.info.context_window, Some(12345)); | |
| assert_eq!(model.thinking_levels, [Max]); | |
| assert_eq!(model.provider_options["openai"]["reasoningEffort"], "max"); | |
| assert!( | |
| model.provider_options["openai"] | |
| .get("reasoningSummary") | |
| .is_none() | |
| ); | |
| assert_eq!(model.main_output_limit, None); | |
| } | |
| #[tokio::test] | |
| async fn catalog_metadata_and_execution_share_provider_facts_and_explicit_overrides() { | |
| let binding = binding("openai"); | |
| for (reported, overrides) in [ | |
| (ModelInfo::new("gpt-5.2"), ModelOverride::default()), | |
| ( | |
| ModelInfo { | |
| thinking_levels: Some(vec![]), | |
| ..ModelInfo::new("gpt-5.2") | |
| }, | |
| ModelOverride::default(), | |
| ), | |
| ( | |
| ModelInfo::new("gpt-5.2"), | |
| ModelOverride { | |
| thinking_levels: Some(vec![Low, High]), | |
| default_thinking_level: Some(High), | |
| ..Default::default() | |
| }, | |
| ), | |
| ] { | |
| let mut input = request(&binding, "gpt-5.2", overrides.clone()); | |
| let metadata = ApiProviders::model_metadata( | |
| "openai", | |
| snapshot(), | |
| input.connection.clone(), | |
| reported.clone(), | |
| Some(overrides), | |
| ) | |
| .unwrap(); | |
| input.model = reported; | |
| let model = binding.prepare(input).await.unwrap(); | |
| assert_eq!( | |
| serde_json::to_value(&model.info).unwrap(), | |
| serde_json::to_value(&metadata.info).unwrap() | |
| ); | |
| assert_eq!(model.thinking_levels, metadata.thinking_levels); | |
| } | |
| } | |
| #[tokio::test] | |
| async fn selectable_thinking_choices_respect_native_encoder_and_invalid_default() { | |
| let binding = binding("anthropic-compatible"); | |
| let mut input = request( | |
| &binding, | |
| "future-model", | |
| ModelOverride { | |
| thinking_levels: Some(vec![High]), | |
| ..Default::default() | |
| }, | |
| ); | |
| let metadata = ApiProviders::model_metadata( | |
| "anthropic-compatible", | |
| snapshot(), | |
| input.connection.clone(), | |
| input.model.clone(), | |
| input.overrides.clone(), | |
| ) | |
| .unwrap(); | |
| assert_eq!(metadata.thinking_levels, [High]); | |
| assert_eq!(metadata.default_thinking_level, None); | |
| assert_eq!( | |
| binding | |
| .prepare(input.clone()) | |
| .await | |
| .unwrap() | |
| .provider_options, | |
| json!({"anthropic":{"cacheControl":{"type":"ephemeral"}}}) | |
| ); | |
| input.thinking_level = Some(High); | |
| assert_eq!( | |
| binding | |
| .prepare(input.clone()) | |
| .await | |
| .unwrap() | |
| .provider_options, | |
| json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"high"}}) | |
| ); | |
| input.overrides.as_mut().unwrap().default_thinking_level = Some(High); | |
| let metadata = ApiProviders::model_metadata( | |
| "anthropic-compatible", | |
| snapshot(), | |
| input.connection.clone(), | |
| input.model.clone(), | |
| input.overrides.clone(), | |
| ) | |
| .unwrap(); | |
| assert_eq!(metadata.thinking_levels, [High]); | |
| assert_eq!(metadata.default_thinking_level, Some(High)); | |
| input.thinking_level = None; | |
| assert_eq!( | |
| binding | |
| .prepare(input.clone()) | |
| .await | |
| .unwrap() | |
| .provider_options, | |
| json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"high"}}) | |
| ); | |
| input.overrides.as_mut().unwrap().thinking_levels = Some(vec![Low]); | |
| assert!( | |
| ApiProviders::model_metadata( | |
| "anthropic-compatible", | |
| snapshot(), | |
| input.connection.clone(), | |
| input.model.clone(), | |
| input.overrides.clone(), | |
| ) | |
| .is_err() | |
| ); | |
| // A default outside the declared choices is invalid on either protocol. | |
| assert!( | |
| ApiProviders::model_metadata( | |
| "openai-compatible", | |
| snapshot(), | |
| input.connection, | |
| input.model, | |
| input.overrides | |
| ) | |
| .is_err() | |
| ); | |
| } |