返回 CodeWhale
test_cases_07.rs
根目录 / crates / tui / src / client / test_cases_07.rs
1
2 #[test]
3 fn untethered_cross_protocol_rebound_fails_closed_without_config() {
4 // #6320: the #5042 early-outs cover the already-exact route, but a
5 // cross-protocol rebound with no Config to rebuild from still fails
6 // closed — this is what still guards half-bound dispatch.
7 let (_config, route) =
8 deepseek_route_for_test("https://api.deepseek.com/beta", "deepseek-v4-pro");
9 let client = CodewhaleClient::new(&route.config).expect("pro client resolves");
10 assert_eq!(client.wire_format, WireFormat::ChatCompletions);
11 let err = match client.rebound_for_model_protocol(None, "deepseek-v4-flash") {
12 Ok(_) => panic!("cross-protocol rebound without config fails closed"),
13 Err(err) => err,
14 };
15 assert!(
16 err.to_string().contains("no configuration is available"),
17 "{err}"
18 );
19 }
20
21 #[test]
22 fn from_candidate_binds_custom_provider_base_url_and_model() {
23 // #1519: a custom OpenAI-compatible provider resolves to a candidate
24 // whose endpoint/model come from the named `[providers.<name>]` table,
25 // and `from_candidate` must bind that verbatim base URL + wire model.
26 let mut custom = std::collections::HashMap::new();
27 custom.insert(
28 "my_thing".to_string(),
29 ProviderConfig {
30 kind: Some("openai-compatible".to_string()),
31 base_url: Some("https://api.example.com/v1".to_string()),
32 model: Some("custom-model-v1".to_string()),
33 api_key_env: Some("EXAMPLE_API_KEY_FROM_CANDIDATE_TEST".to_string()),
34 ..Default::default()
35 },
36 );
37 let config = Config {
38 provider: Some("my_thing".to_string()),
39 providers: Some(ProvidersConfig {
40 custom,
41 ..Default::default()
42 }),
43 ..Config::default()
44 };
45
46 // The config names a custom provider, so it must resolve as Custom.
47 assert_eq!(
48 config.active_provider_identity().unwrap().provider,
49 ProviderKind::Custom
50 );
51
52 let route = crate::route_runtime::resolve_runtime_route(&config, ProviderKind::Custom, None)
53 .expect("custom route should resolve");
54
55 // Provide the key the route's auth path will read.
56 let client = {
57 let _env = crate::test_support::lock_test_env();
58 let _key = crate::test_support::EnvVarGuard::set(
59 "EXAMPLE_API_KEY_FROM_CANDIDATE_TEST",
60 "sk-custom",
61 );
62 CodewhaleClient::from_candidate(&route.config, &route.candidate)
63 .expect("client should construct from custom candidate")
64 };
65
66 assert_eq!(client.base_url, "https://api.example.com/v1");
67 assert_eq!(client.default_model, "custom-model-v1");
68 assert_eq!(client.api_provider, ProviderKind::Custom);
69 // The candidate carried the custom endpoint + verbatim wire model.
70 assert_eq!(
71 route.candidate.endpoint().base_url,
72 "https://api.example.com/v1"
73 );
74 assert_eq!(route.candidate.wire_model_id().as_str(), "custom-model-v1");
75 }
76 #[tokio::test]
77 async fn incomplete_translation_keeps_exact_route_and_usage_before_rejection() {
78 let server = MockServer::start().await;
79 Mock::given(method("POST"))
80 .and(path("/v1/messages"))
81 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
82 "id": "msg_partial",
83 "type": "message",
84 "role": "assistant",
85 "content": [{"type": "text", "text": "Parcial"}],
86 // A provider-returned alias must not replace the admitted
87 // route/model in the frozen cost receipt.
88 "model": "provider-alias-after-dispatch",
89 "stop_reason": "max_tokens",
90 "stop_sequence": null,
91 "usage": {"input_tokens": 7, "output_tokens": 2}
92 })))
93 .expect(1)
94 .mount(&server)
95 .await;
96
97 let client = deepseek_anthropic_client(&server);
98 let response = client
99 .translate_with_usage("Hello", "deepseek-chat", "Spanish")
100 .await
101 .expect("decoded provider response retains its receipt");
102
103 assert!(
104 response.translated.is_err(),
105 "partial text must be rejected"
106 );
107 let usage = response.usage.expect("provider-reported usage");
108 assert_eq!(usage.input_tokens, 7);
109 assert_eq!(usage.output_tokens, 2);
110 assert_eq!(response.route.provider, ProviderKind::DeepseekAnthropic);
111 assert_eq!(response.route.model, "deepseek-chat");
112 assert_eq!(response.route.provider_identity, "deepseek-anthropic");
113 assert!(response.route.endpoint_fingerprint.is_some());
114 }
115
116 #[tokio::test]
117 async fn chat_translation_without_usage_keeps_unreceipted_success_outcome() {
118 let server = MockServer::start().await;
119 Mock::given(method("POST"))
120 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
121 "id": "chatcmpl-no-usage",
122 "model": "deepseek-chat",
123 "choices": [{
124 "index": 0,
125 "message": {"role": "assistant", "content": "Hola"},
126 "finish_reason": "stop"
127 }]
128 })))
129 .expect(1)
130 .mount(&server)
131 .await;
132
133 let client = deepseek_request_boundary_client("https://api.deepseek.com/v1", server.uri());
134 let response = client
135 .translate_with_usage("Hello", "deepseek-chat", "Spanish")
136 .await
137 .expect("provider success must retain its frozen route");
138 assert_eq!(
139 response
140 .translated
141 .expect("useful output remains deliverable"),
142 "Hola"
143 );
144 assert_eq!(response.usage, None, "must not mint a priced-zero receipt");
145 assert_eq!(response.route.provider, ProviderKind::Deepseek);
146 assert_eq!(
147 response.route.model,
148 wire_model_for_provider_route(
149 ProviderKind::Deepseek,
150 "https://api.deepseek.com/v1",
151 "deepseek-chat"
152 )
153 );
154 }
155
156 #[tokio::test]
157 async fn chat_translation_http_error_is_not_a_provider_success_outcome() {
158 let server = MockServer::start().await;
159 Mock::given(method("POST"))
160 .respond_with(ResponseTemplate::new(429).set_body_json(json!({
161 "error": {"message": "rate limited"}
162 })))
163 .mount(&server)
164 .await;
165
166 let client = deepseek_request_boundary_client("https://api.deepseek.com/v1", server.uri());
167 let error = match client
168 .translate_with_usage("Hello", "deepseek-chat", "Spanish")
169 .await
170 {
171 Ok(_) => panic!("HTTP failure must not become a provider-success receipt"),
172 Err(error) => error,
173 };
174 let display = error.to_string();
175 assert!(
176 display.to_ascii_lowercase().contains("rate limit"),
177 "{display}"
178 );
179 assert!(!display.contains("chatcmpl"), "{display}");
180 }
181
182 #[tokio::test]
183 async fn custom_catalog_schema_ignores_table_name() {
184 // The same enriched body served from a non-Baseten endpoint takes
185 // the generic branch for every identity: the table name selects
186 // ownership, never the wire parser (#6289). Baseten enrichment
187 // itself stays covered at the parser's fixture tests.
188 let server = MockServer::start().await;
189 Mock::given(method("GET"))
190 .and(path("/v1/models"))
191 .and(header("authorization", "Bearer test-custom-key"))
192 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
193 "data": [{
194 "id": "synthetic/custom-model",
195 "context_length": 1_048_576,
196 "pricing": {
197 "prompt": 0.0000014,
198 "completion": 0.0000044
199 },
200 "supported_features": ["reasoning", "tools", "structured_outputs", "vision"]
201 }]
202 })))
203 .mount(&server)
204 .await;
205
206 for identity in ["baseten", "renamed-host"] {
207 let client = custom_mock_client_for_identity(&server, identity);
208 assert_eq!(client.catalog_provider_id(), identity);
209 let delta = client.fetch_catalog_delta().await.expect("delta");
210 assert_eq!(delta.provider, identity);
211 assert_eq!(delta.offerings.len(), 1);
212 let offering = &delta.offerings[0];
213 assert_eq!(offering.wire_model_id, "synthetic/custom-model");
214 assert_eq!(offering.provider, identity);
215 assert!(
216 offering.limit.is_none() && offering.cost.is_none() && offering.tool_call.is_none(),
217 "a non-Baseten endpoint takes the generic branch for any identity: {offering:?}"
218 );
219 }
220 }
221
222 #[test]
223 fn baseten_dialect_recognized_by_endpoint_not_name() {
224 fn client_for(identity: &str, base_url: &str) -> CodewhaleClient {
225 let mut providers = ProvidersConfig::default();
226 providers.custom.insert(
227 identity.to_string(),
228 ProviderConfig {
229 kind: Some("openai-compatible".to_string()),
230 api_key: Some("test-key".to_string()),
231 base_url: Some(base_url.to_string()),
232 model: Some("synthetic/custom-model".to_string()),
233 ..ProviderConfig::default()
234 },
235 );
236 CodewhaleClient::new(&Config {
237 provider: Some(identity.to_string()),
238 providers: Some(providers),
239 ..Config::default()
240 })
241 .expect("client")
242 }
243
244 let baseten_url = codewhale_config::catalog::BASETEN_BASE_URL;
245 assert!(client_for("baseten", baseten_url).catalog_endpoint_is_baseten());
246 assert!(client_for("renamed-host", baseten_url).catalog_endpoint_is_baseten());
247 assert!(
248 client_for("baseten", &format!("{baseten_url}/")).catalog_endpoint_is_baseten(),
249 "a trailing slash still recognizes the host"
250 );
251 assert!(!client_for("baseten", "https://127.0.0.1:9/v1").catalog_endpoint_is_baseten());
252 assert!(!client_for("groq", "https://api.groq.com/openai/v1").catalog_endpoint_is_baseten());
253 }
254
254 lines RUST