1use super::*;
4
5mod parsing;
6mod streaming;
7
8impl MergeGatewayProvider {
9 pub fn new(api_key: String) -> Self {
10 Self::with_model(api_key, models::merge_gateway::DEFAULT_MODEL.to_string())
11 }
12
13 pub fn with_model(api_key: String, model: String) -> Self {
14 let timeouts = TimeoutsConfig::default();
15 let http_client = HttpClientFactory::for_llm(&timeouts);
16 Self::from_runtime_parts(api_key, model, http_client, urls::MERGE_GATEWAY_NATIVE_API_BASE.to_string())
17 }
18
19 pub fn new_with_client(
20 api_key: String,
21 model: String,
22 http_client: HttpClient,
23 base_url: String,
24 _timeouts: TimeoutsConfig,
25 ) -> Self {
26 Self::from_runtime_parts(api_key, model, http_client, base_url)
27 }
28
29 pub fn from_config(
30 api_key: Option<String>,
31 model: Option<String>,
32 base_url: Option<String>,
33 prompt_cache: Option<PromptCachingConfig>,
34 timeouts: Option<TimeoutsConfig>,
35 _anthropic: Option<AnthropicConfig>,
36 model_behavior: Option<ModelConfig>,
37 ) -> Self {
38 let api_key = <MergeGatewaySpec as OpenAiCompatSpec>::resolve_api_key(api_key);
39 let model = resolve_model(model, models::merge_gateway::DEFAULT_MODEL);
40 let base_url =
41 override_base_url(urls::MERGE_GATEWAY_NATIVE_API_BASE, base_url, Some(env_vars::MERGE_GATEWAY_BASE_URL));
42 let timeouts = timeouts.unwrap_or_default();
43 let http_client = HttpClientFactory::for_llm(&timeouts);
44
45 let legacy_core = if is_legacy_openai_base_url(&base_url) {
46 Some(OpenAiCompatCore::from_config(
47 Some(api_key.clone()),
48 Some(model.clone()),
49 Some(base_url.clone()),
50 prompt_cache,
51 Some(timeouts),
52 model_behavior.clone(),
53 ))
54 } else {
55 None
56 };
57
58 Self {
59 native: NativeMergeGatewayCore {
60 api_key,
61 http_client,
62 base_url,
63 model,
64 model_behavior,
65 },
66 legacy_core,
67 no_tool_vendor_cache: Mutex::new(HashSet::new()),
68 }
69 }
70
71 fn from_runtime_parts(api_key: String, model: String, http_client: HttpClient, base_url: String) -> Self {
72 let legacy_core = if is_legacy_openai_base_url(&base_url) {
73 Some(OpenAiCompatCore::from_parts(api_key.clone(), model.clone(), http_client.clone(), base_url.clone()))
74 } else {
75 None
76 };
77
78 Self {
79 native: NativeMergeGatewayCore {
80 api_key,
81 http_client,
82 base_url,
83 model,
84 model_behavior: None,
85 },
86 legacy_core,
87 no_tool_vendor_cache: Mutex::new(HashSet::new()),
88 }
89 }
90
91 fn prepare_native_request(&self, request: &mut LLMRequest) {
92 if request.model.trim().is_empty() {
93 request.model = self.native.model.clone();
94 }
95 }
96
97 fn tool_vendor_known_missing(&self, model: &str) -> bool {
98 self.no_tool_vendor_cache
99 .lock()
100 .map(|guard| guard.contains(model))
101 .unwrap_or(false)
102 }
103
104 pub(crate) fn mark_tool_vendor_missing(&self, model: &str) {
105 if let Ok(mut guard) = self.no_tool_vendor_cache.lock() {
106 guard.insert(model.to_owned());
107 }
108 }
109
110 fn no_tool_vendor_cached_error(model: &str) -> LLMError {
111 provider_error(format!(
112 "Model '{model}' has no vendor serving tools on Merge Gateway (cached from an earlier capability_unavailable rejection). Hint: use default_routing or another model until the route gains a tool-capable vendor."
113 ))
114 }
115
116 fn responses_url(&self) -> String {
117 format!("{}/responses", self.native.base_url.trim_end_matches('/'))
118 }
119
120 fn build_native_tools(&self, tools: &[ToolDefinition], model: &str) -> Option<Vec<Value>> {
121 let gemini_compatible = model.starts_with("google/gemini-");
122 let serialized: Vec<Value> = tools
123 .iter()
124 .filter_map(|tool| {
125 tool.function.as_ref().map(|func| {
126 let parameters = if gemini_compatible {
127 sanitize_function_parameters(func.parameters.clone())
128 } else {
129 func.parameters.clone()
130 };
131 json!({
132 "type": "function",
133 "name": func.name,
134 "description": func.description,
135 "parameters": parameters,
136 })
137 })
138 })
139 .collect();
140
141 if serialized.is_empty() { None } else { Some(serialized) }
142 }
143
144 fn native_tool_use_value(&self, call: &ToolCall) -> Result<Value, LLMError> {
145 call.validate()
146 .map_err(|err| provider_error(format!("Invalid Merge Gateway tool call: {err}")))?;
147 let name = call
148 .tool_name()
149 .ok_or_else(|| provider_error("Merge Gateway tool call is missing a function name"))?;
150 let input = call
151 .execution_arguments()
152 .map_err(|err| provider_error(format!("Failed to serialize Merge Gateway tool call arguments: {err}")))?;
153 Ok(json!({
154 "type": "tool_use",
155 "id": call.id,
156 "name": name,
157 "input": input,
158 }))
159 }
160
161 fn native_message_content_value(&self, message: &Message, model: &str) -> Result<Value, LLMError> {
162 let content = serialize_message_content_openai_for_model(message, model);
163 if message.tool_calls.as_ref().is_none_or(Vec::is_empty) {
164 return Ok(content);
165 }
166
167 let mut parts = match content {
168 Value::String(text) => {
169 if text.trim().is_empty() {
170 Vec::new()
171 } else {
172 vec![json!({"type": "text", "text": text})]
173 }
174 }
175 Value::Array(parts) => parts,
176 other => vec![other],
177 };
178
179 if let Some(tool_calls) = &message.tool_calls {
180 for call in tool_calls {
181 parts.push(self.native_tool_use_value(call)?);
182 }
183 }
184
185 Ok(Value::Array(parts))
186 }
187
188 fn native_input_item_for_message(&self, message: &Message, model: &str) -> Result<Value, LLMError> {
189 if message.role.is_tool_response() {
190 let tool_call_id = message
191 .tool_call_id
192 .clone()
193 .ok_or_else(|| provider_error("Merge Gateway tool result messages must include a tool_call_id"))?;
194 return Ok(json!({
195 "type": "tool_result",
196 "tool_use_id": tool_call_id,
197 "content": function_output_value_from_message_content(&message.content),
198 }));
199 }
200
201 Ok(json!({
202 "type": "message",
203 "role": message.role.as_generic_str(),
204 "content": self.native_message_content_value(message, model)?,
205 }))
206 }
207
208 fn native_stop_sequences(request: &LLMRequest) -> Option<Vec<Value>> {
209 let stop: Vec<Value> = request
210 .stop_sequences
211 .as_ref()
212 .into_iter()
213 .flatten()
214 .filter_map(|stop| {
215 let trimmed = stop.trim();
216 if trimmed.is_empty() {
217 None
218 } else {
219 Some(Value::String(trimmed.to_string()))
220 }
221 })
222 .collect();
223
224 if stop.is_empty() { None } else { Some(stop) }
225 }
226
227 pub(crate) fn build_native_payload(&self, request: &LLMRequest, stream: bool) -> Result<Value, LLMError> {
228 let mut input = Vec::new();
229
230 if let Some(system) = &request.system_prompt {
231 let trimmed = system.trim();
232 if !trimmed.is_empty() {
233 input.push(json!({
234 "type": "message",
235 "role": "system",
236 "content": trimmed,
237 }));
238 }
239 }
240
241 for message in request.messages.iter() {
242 input.push(self.native_input_item_for_message(message, &request.model)?);
243 }
244
245 let mut payload = Map::new();
246 payload.insert("model".to_owned(), Value::String(request.model.clone()));
247 payload.insert("input".to_owned(), Value::Array(input));
248
249 let tools_disabled = matches!(request.tool_choice, Some(ToolChoice::None));
252 if !tools_disabled
253 && let Some(tools) = request
254 .tools
255 .as_ref()
256 .and_then(|tools| self.build_native_tools(tools, &request.model))
257 {
258 payload.insert("tools".to_owned(), Value::Array(tools));
259 }
260
261 if let Some(max_tokens) = request.max_tokens {
262 payload.insert("max_tokens".to_owned(), json!(max_tokens));
263 }
264 if let Some(temperature) = request.temperature {
265 payload.insert("temperature".to_owned(), json!(crate::providers::common::sampling_param_f64(temperature)));
266 }
267 if let Some(top_p) = request.top_p {
268 payload.insert("top_p".to_owned(), json!(crate::providers::common::sampling_param_f64(top_p)));
269 }
270 if let Some(stop) = Self::native_stop_sequences(request) {
271 payload.insert("stop".to_owned(), Value::Array(stop));
272 }
273 if !tools_disabled && let Some(choice) = &request.tool_choice {
274 payload.insert("tool_choice".to_owned(), choice.to_provider_format("merge-gateway"));
275 }
276 if let Some(output_format) = &request.output_format {
277 payload.insert("response_format".to_owned(), output_format.clone());
278 }
279 if let Some(service_tier) = request.service_tier.as_deref().map(str::trim).filter(|value| !value.is_empty()) {
280 match map_openai_service_tier_for_merge(service_tier) {
281 Some(mapped) => {
282 payload.insert("service_tier".to_owned(), Value::String(mapped.to_owned()));
283 }
284 None => {
285 tracing::warn!(
286 model = %request.model,
287 service_tier = %service_tier,
288 "Merge Gateway has no equivalent for service_tier; omitting it and using gateway default routing"
289 );
290 }
291 }
292 }
293 if let Some(cache_key) = request
294 .prompt_cache_key
295 .as_deref()
296 .map(str::trim)
297 .filter(|value| !value.is_empty())
298 {
299 payload.insert("prompt_cache_key".to_owned(), Value::String(cache_key.to_owned()));
300 }
301 if let Some(session_id) = merge_session_identity(request) {
302 payload.insert("session_id".to_owned(), Value::String(session_id));
303 }
304 if stream {
305 payload.insert("stream".to_owned(), Value::Bool(true));
306 }
307
308 if let Some(effort) = request.reasoning_effort
309 && is_active_reasoning_effort(effort)
310 && let Some(control) = merge_reasoning_control_for_model(&request.model)
311 {
312 match control {
313 MergeReasoningControl::ReasoningEffort => {
314 payload.insert("reasoning_effort".to_owned(), Value::String(effort.as_str().to_owned()));
315 }
316 MergeReasoningControl::ThinkingBudget => {
317 if let Some(thinking) = merge_thinking_payload(effort, request.max_tokens) {
318 payload.insert("thinking".to_owned(), thinking);
319 }
320 }
321 }
322 }
323
324 Ok(Value::Object(payload))
325 }
326
327 async fn generate_native(&self, request: LLMRequest) -> Result<LLMResponse, LLMError> {
328 self.generate_native_with_streaming_retry(request, true).await
329 }
330
331 async fn generate_native_with_streaming_retry(
332 &self,
333 mut request: LLMRequest,
334 allow_streaming_retry: bool,
335 ) -> Result<LLMResponse, LLMError> {
336 self.prepare_native_request(&mut request);
337 LLMProvider::validate_request(self, &request)?;
338 if self.tool_vendor_known_missing(&request.model) && native_request_sends_tools(&request) {
342 return Err(Self::no_tool_vendor_cached_error(&request.model));
343 }
344 let payload = self.build_native_payload(&request, false)?;
345 let session_id = merge_session_identity(&request);
346 let mut http = self
347 .native
348 .http_client
349 .post(self.responses_url())
350 .bearer_auth(&self.native.api_key);
351 if let Some(session_id) = session_id.as_deref() {
352 http = http.header("X-Session-Id", session_id);
353 }
354 let response = http
355 .json(&payload)
356 .send()
357 .await
358 .map_err(|e| format_network_error("Merge Gateway", &e))?;
359
360 if !response.status().is_success() {
361 let status = response.status();
362 let body = crate::providers::common::read_provider_error_body(response).await;
363 if is_merge_tier_pricing_rejection(status, &body) && request.service_tier.is_some() {
367 tracing::warn!(
368 model = %request.model,
369 status = %status,
370 "Merge Gateway route is not priced for service_tier; retrying once without it"
371 );
372 request.service_tier = None;
373 return Box::pin(self.generate_native_with_streaming_retry(request, allow_streaming_retry)).await;
374 }
375 if allow_streaming_retry && is_streaming_only_rejection(status, &body) {
381 tracing::warn!(
382 model = %request.model,
383 status = %status,
384 "Merge Gateway route requires streaming; retrying once with streaming"
385 );
386 return Box::pin(self.collect_streamed_response(request)).await;
387 }
388 let sent_reasoning = request.reasoning_effort.is_some_and(is_active_reasoning_effort);
396 if is_capability_unavailable(status, &body)
397 && native_request_sends_tools(&request)
398 && !(sent_reasoning && is_reasoning_capability_rejection(&body))
399 {
400 self.mark_tool_vendor_missing(&request.model);
401 }
402 return Err(merge_request_error(status, &body));
403 }
404
405 let json: Value = response.json().await.map_err(|e| format_parse_error("Merge Gateway", &e))?;
406 Self::parse_native_response_payload(json, request.model)
407 }
408
409 async fn collect_streamed_response(&self, request: LLMRequest) -> Result<LLMResponse, LLMError> {
410 let mut stream = self.stream_native_normalized_inner(request, false).await?;
411 let mut completed = None;
412 while let Some(event) = stream.next().await {
413 match event? {
414 NormalizedStreamEvent::Done { response } => {
415 completed = Some(*response);
416 break;
417 }
418 NormalizedStreamEvent::Usage { .. }
419 | NormalizedStreamEvent::TextDelta { .. }
420 | NormalizedStreamEvent::ReasoningDelta { .. }
421 | NormalizedStreamEvent::ReasoningStage { .. }
422 | NormalizedStreamEvent::ToolCallStart { .. }
423 | NormalizedStreamEvent::ToolCallDelta { .. } => {}
424 }
425 }
426 completed.ok_or_else(|| provider_error("Merge Gateway streamed fallback ended without a completed response"))
427 }
428
429 async fn stream_native_normalized(&self, request: LLMRequest) -> Result<LLMNormalizedStream, LLMError> {
430 self.stream_native_normalized_inner(request, true).await
431 }
432
433 async fn stream_native_normalized_inner(
434 &self,
435 mut request: LLMRequest,
436 allow_non_streaming_fallback: bool,
437 ) -> Result<LLMNormalizedStream, LLMError> {
438 self.prepare_native_request(&mut request);
439 LLMProvider::validate_request(self, &request)?;
440 if self.tool_vendor_known_missing(&request.model) && native_request_sends_tools(&request) {
441 return Err(Self::no_tool_vendor_cached_error(&request.model));
442 }
443 request.stream = true;
444
445 let payload = self.build_native_payload(&request, true)?;
446 let session_id = merge_session_identity(&request);
447 let mut http = self
448 .native
449 .http_client
450 .post(self.responses_url())
451 .bearer_auth(&self.native.api_key);
452 if let Some(session_id) = session_id.as_deref() {
453 http = http.header("X-Session-Id", session_id);
454 }
455 let response = http
456 .json(&payload)
457 .send()
458 .await
459 .map_err(|e| format_network_error("Merge Gateway", &e))?;
460
461 if !response.status().is_success() {
462 let status = response.status();
463 let body = crate::providers::common::read_provider_error_body(response).await;
464 if allow_non_streaming_fallback
474 && is_capability_unavailable(status, &body)
475 && !is_streaming_only_model(&request.model)
476 {
477 tracing::warn!(
478 model = %request.model,
479 status = %status,
480 "Merge Gateway rejected streaming capabilities; retrying once without streaming"
481 );
482 request.stream = false;
483 let fallback = Box::pin(self.generate_native_with_streaming_retry(request, false)).await?;
484 let completed = LLMStreamEvent::Completed { response: Box::new(fallback) };
485 let stream = try_stream! {
486 for event in completed.into_normalized() {
487 yield event;
488 }
489 };
490 return Ok(Box::pin(stream));
491 }
492 if is_merge_tier_pricing_rejection(status, &body) && request.service_tier.is_some() {
495 tracing::warn!(
496 model = %request.model,
497 status = %status,
498 "Merge Gateway route is not priced for service_tier; retrying once without it"
499 );
500 request.service_tier = None;
501 return Box::pin(self.stream_native_normalized_inner(request, allow_non_streaming_fallback)).await;
502 }
503 return Err(merge_request_error(status, &body));
504 }
505
506 let model = request.model.clone();
507 let stream = try_stream! {
508 let mut body_stream = response.bytes_stream();
509 let mut decoder = Utf8StreamDecoder::new();
510 let mut buffer: Vec<u8> = Vec::new();
511 let mut offset = 0usize;
512 let mut state = MergeStreamState::new(model.clone());
513
514 while let Some(chunk_result) = body_stream.next().await {
515 let chunk = chunk_result.map_err(|e| format_network_error("Merge Gateway", &e))?;
516 decoder.push_bytes(&chunk, &mut buffer);
517
518 while let Some(raw_event) = next_sse_event(&buffer, &mut offset)
519 .map_err(|error| format_parse_error("Merge Gateway", &error))?
520 {
521
522 let payload_text = match extract_data_payload(raw_event) {
523 Some(payload) => payload.into_owned(),
524 None => {
525 let trimmed = raw_event.trim();
526 if trimmed.starts_with('{') || trimmed.starts_with('[') {
527 trimmed.to_string()
528 } else {
529 continue;
530 }
531 }
532 };
533
534 if payload_text.trim() == "[DONE]" {
535 state.done = true;
536 break;
537 }
538
539 let events = Self::handle_native_stream_payload(raw_event, &payload_text, &mut state)?;
540 for event in events {
541 yield event;
542 }
543
544 if state.done {
545 break;
546 }
547 }
548
549 if offset > 0 {
550 buffer.drain(..offset);
551 offset = 0;
552 }
553
554 if state.done {
555 break;
556 }
557 }
558
559 if !state.done {
560 Err(provider_error("Merge Gateway stream ended before a terminal response event"))?;
561 }
562
563 if let Some(response) = state.native_snapshot.take() {
564 for event in state.apply_native_snapshot(response)? {
565 yield event;
566 }
567 }
568
569 let response = state.finish()?;
570 if let Some(usage) = response.usage.clone() {
571 yield NormalizedStreamEvent::Usage { usage };
572 }
573 yield NormalizedStreamEvent::Done { response: Box::new(response) };
574 };
575
576 Ok(Box::pin(stream))
577 }
578}
579
580#[async_trait]
583impl LLMProvider for MergeGatewayProvider {
584 fn name(&self) -> &str {
585 "merge-gateway"
586 }
587
588 async fn generate(&self, mut request: LLMRequest) -> Result<LLMResponse, LLMError> {
589 if let Some(core) = &self.legacy_core {
590 core.prepare(&mut request);
591 return core.generate_prepared(request).await;
592 }
593
594 self.generate_native(request).await
595 }
596
597 async fn stream(&self, request: LLMRequest) -> Result<LLMStream, LLMError> {
598 if let Some(core) = &self.legacy_core {
599 let mut request = request;
600 core.prepare(&mut request);
601 LLMProvider::validate_request(self, &request)?;
602 request.stream = true;
603 return core.stream_prepared(request).await;
604 }
605
606 let mut normalized = self.stream_native_normalized(request).await?;
607 let stream = try_stream! {
608 let mut completed = None;
609 while let Some(event) = normalized.next().await {
610 match event? {
611 NormalizedStreamEvent::TextDelta { delta } => yield LLMStreamEvent::Token { delta },
612 NormalizedStreamEvent::ReasoningDelta { delta, .. } => yield LLMStreamEvent::Reasoning { delta },
613 NormalizedStreamEvent::ReasoningStage { stage } => yield LLMStreamEvent::ReasoningStage { stage },
614 NormalizedStreamEvent::ToolCallStart { .. }
615 | NormalizedStreamEvent::ToolCallDelta { .. }
616 | NormalizedStreamEvent::Usage { .. } => {}
617 NormalizedStreamEvent::Done { response } => {
618 completed = Some(response);
619 break;
620 }
621 }
622 }
623
624 if let Some(response) = completed {
625 yield LLMStreamEvent::Completed { response };
626 }
627 };
628
629 Ok(Box::pin(stream))
630 }
631
632 async fn stream_normalized(&self, request: LLMRequest) -> Result<LLMNormalizedStream, LLMError> {
633 if self.legacy_core.is_some() {
634 let mut legacy_stream = self.stream(request).await?;
635 let stream = try_stream! {
636 while let Some(event) = legacy_stream.next().await {
637 for normalized in event?.into_normalized() {
638 yield normalized;
639 }
640 }
641 };
642 return Ok(Box::pin(stream));
643 }
644
645 self.stream_native_normalized(request).await
646 }
647
648 fn supported_models(&self) -> Vec<String> {
649 models::merge_gateway::SUPPORTED_MODELS
650 .iter()
651 .map(|model| (*model).to_string())
652 .collect()
653 }
654
655 fn validate_request(&self, request: &LLMRequest) -> Result<(), LLMError> {
656 validate_request_common(request, "Merge Gateway", "merge-gateway", None)
657 }
658
659 fn supports_streaming(&self) -> bool {
660 true
661 }
662
663 fn supports_non_streaming(&self, model: &str) -> bool {
664 !is_streaming_only_model(model)
672 }
673
674 fn supports_structured_output(&self, _model: &str) -> bool {
675 false
676 }
677
678 fn supports_reasoning(&self, model: &str) -> bool {
679 self.native
680 .model_behavior
681 .as_ref()
682 .and_then(|behavior| behavior.model_supports_reasoning)
683 .unwrap_or_else(|| merge_reasoning_control_for_model(model).is_some())
684 }
685
686 fn supports_reasoning_effort(&self, model: &str) -> bool {
687 self.native
688 .model_behavior
689 .as_ref()
690 .and_then(|behavior| behavior.model_supports_reasoning_effort)
691 .unwrap_or_else(|| merge_reasoning_control_for_model(model).is_some())
692 }
693
694 fn supports_vision(&self, model: &str) -> bool {
695 matches!(
696 model,
697 models::merge_gateway::ANTHROPIC_CLAUDE_OPUS_5
698 | models::merge_gateway::ANTHROPIC_CLAUDE_OPUS_5_5
699 | models::merge_gateway::ANTHROPIC_CLAUDE_SONNET_5
700 | models::merge_gateway::ANTHROPIC_CLAUDE_SONNET_5_5
701 | models::merge_gateway::ANTHROPIC_CLAUDE_HAIKU_4_5_20251001
702 | models::merge_gateway::ANTHROPIC_CLAUDE_HAIKU_5_5
703 | models::merge_gateway::ANTHROPIC_CLAUDE_FABLE_5_1
704 | models::merge_gateway::GOOGLE_GEMINI_3_6_FLASH
705 | models::merge_gateway::GOOGLE_GEMINI_3_7_FLASH
706 | models::merge_gateway::GOOGLE_GEMINI_3_8_FLASH
707 | models::merge_gateway::GOOGLE_GEMINI_4_ARGON
708 | models::merge_gateway::MOONSHOT_KIMI_K3
709 | models::merge_gateway::MOONSHOT_KIMI_K3_1_PRO
710 | models::merge_gateway::ZAI_GLM_5_3_FLASH
711 | models::merge_gateway::OPENAI_GPT_5_5
712 | models::merge_gateway::OPENAI_GPT_5_6_LUNA
713 | models::merge_gateway::OPENAI_GPT_5_6_SOL
714 | models::merge_gateway::OPENAI_GPT_5_6_TERRA
715 | models::merge_gateway::OPENAI_GPT_6_ASTRA
716 | models::merge_gateway::OPENAI_GPT_6_SOL
717 | models::merge_gateway::OPENAI_GPT_6_1_SOL
718 | models::merge_gateway::OPENAI_GPT_6_1_TERRA
719 | models::merge_gateway::OPENAI_GPT_6_LUNA
720 | models::merge_gateway::META_MUSE_SPARK_OPEN
721 )
722 }
723
724 fn effective_context_size(&self, model: &str) -> usize {
725 vtcode_config::models::model_catalog_entry(MergeGatewaySpec::KEY, model)
726 .map(|entry| entry.context_window)
727 .filter(|capacity| *capacity > 0)
728 .unwrap_or(128_000)
729 }
730}
731
732#[async_trait]
733impl crate::client::LLMClient for MergeGatewayProvider {
734 async fn generate(&mut self, prompt: &str) -> Result<LLMResponse, LLMError> {
735 let request = crate::providers::common::make_default_request(prompt, &self.native.model);
736 Ok(<Self as LLMProvider>::generate(self, request).await?)
737 }
738
739 fn model_id(&self) -> &str {
740 &self.native.model
741 }
742}