Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
47 KiB
JSON
1 line
47 KiB
JSON
{"repo": "atelier-fashion/teton-code", "files": {"crates/teton-providers/src/openai_compat.rs": {"44972057167ba1341cea1933b05f71184dfe7141": {"t": 1785746242.2644677, "n": 11376, "h": "2de6d291ade690d8", "c": "//! The OpenAI-compatible chat/completions adapter.\n//!\n//! Covers any endpoint speaking the OpenAI chat/completions streaming protocol —\n//! DeepSeek, Kimi, Ollama, vLLM, and the like — with the endpoint configurable\n//! so a new provider is registerable with no code change (BR-6). The response is\n//! a stream of `data:` chunks terminated by `data: [DONE]`; `choices[0].delta`\n//! carries text (`content`) and tool-call fragments (`tool_calls[].function.\n//! arguments`, accumulated per index), and a final usage-only chunk carries\n//! `prompt_tokens` / `completion_tokens`. All of it normalizes to the same\n//! [`TurnEvent`]s the Anthropic adapter emits, in the same order.\n//!\n//! Because compat endpoints vary wildly in tool-call reliability, the default\n//! capability tier is conservative (`Degraded`, i.e. the reduced BR-6 harness);\n//! callers that trust a specific endpoint raise it explicitly.\n\nuse crate::capability::CapabilityProfile;\nuse crate::sse::{SseEvent, SseFramer};\nuse crate::transport::{ByteStream, HttpMethod, Transport, TransportRequest};\nuse crate::{\n finalize_tool, PartialTool, Provider, ProviderError, StopReason, TokenUsage, TurnCompletion,\n TurnEvent, TurnRequest, TurnStream,\n};\nuse async_trait::async_trait;\nuse futures::StreamExt;\nuse serde_json::{json, Value};\nuse teton_core::ToolCallTier;\n\n/// Configuration for an OpenAI-compatible provider instance.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct OpenAiCompatConfig {\n /// Stable provider id (feeds `provider_degraded`).\n pub id: String,\n /// Absolute chat/completions endpoint URL (e.g.\n /// `https://api.deepseek.com/v1/chat/completions`).\n pub endpoint: String,\n /// Capability profile. Defaults to the conservative `Degraded` tier for an\n /// unknown compat endpoint; raise it for a trusted provider.\n pub capabilities: CapabilityProfile,\n}\n\nimpl OpenAiCompatConfig {\n /// Create a config for `id` calling `endpoint`, with the conservative\n /// default capability profile (`Degraded` tier).\n #[must_use]\n pub fn new(id: impl Into<String>, endpoint: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n endpoint: endpoint.into(),\n capabilities: CapabilityProfile {\n tool_call_tier: ToolCallTier::Degraded,\n parallel_calls: false,\n max_context: 0,\n },\n }\n }\n\n /// Override the capability profile.\n #[must_use]\n pub fn with_capabilities(mut self, capabilities: CapabilityProfile) -> Self {\n self.capabilities = capabilities;\n self\n }\n}\n\n/// Adapter for any OpenAI-compatible chat/completions endpoint.\n#[derive(Debug, Clone)]\npub struct OpenAiCompatAdapter {\n config: OpenAiCompatConfig,\n}\n\nimpl OpenAiCompatAdapter {\n /// Create an adapter from a config.\n #[must_use]\n pub fn new(config: OpenAiCompatConfig) -> Self {\n Self { config }\n }\n\n fn build_request(&self, req: &TurnRequest) -> Result<TransportRequest, ProviderError> {\n let mut messages: Vec<Value> = Vec::with_capacity(req.messages.len() + 1);\n if let Some(system) = &req.system {\n messages.push(json!({\"role\": \"system\", \"content\": system}));\n }\n for m in &req.messages {\n messages.push(json!({\"role\": m.role.openai_str(), \"content\": m.content.as_str()}));\n }\n\n let mut body = json!({\n \"model\": req.model,\n \"messages\": messages,\n \"stream\": true,\n // Ask for usage in the terminal chunk so Completed always carries it\n // (BR-2).\n \"stream_options\": {\"include_usage\": true},\n \"max_tokens\": req.max_tokens,\n });\n if !req.tools.is_empty() {\n let tools: Vec<Value> = req\n .tools\n .iter()\n .map(|t| {\n json!({\n \"type\": \"function\",\n \"function\": {\n \"name\": t.name,\n \"description\": t.description,\n \"parameters\": t.input_schema,\n }\n })\n })\n .collect();\n body[\"tools\"] = json!(tools);\n }\n\n let body = serde_json::to_vec(&body).map_err(|e| ProviderError::Build(e.to_string()))?;\n Ok(TransportRequest {\n method: HttpMethod::Post,\n url: self.config.endpoint.clone(),\n // Auth header is added by the Transport (egress), not here (BR-7).\n headers: vec![\n (\"content-type\".to_string(), \"application/json\".to_string()),\n (\"accept\".to_string(), \"text/event-stream\".to_string()),\n ],\n body,\n })\n }\n}\n\n#[async_trait]\nimpl Provider for OpenAiCompatAdapter {\n fn id(&self) -> &str {\n &self.config.id\n }\n\n fn capabilities(&self) -> CapabilityProfile {\n self.config.capabilities\n }\n\n async fn stream_turn(\n &self,\n request: TurnRequest,\n transport: &dyn Transport,\n ) -> Result<TurnStream, ProviderError> {\n let http = self.build_request(&request)?;\n let resp = transport\n .execute(http)\n .await\n .map_err(ProviderError::from_transport)?;\n if resp.status >= 500 {\n return Err(ProviderError::ServerError {\n status: resp.status,\n });\n }\n if resp.status >= 400 {\n return Err(ProviderError::ClientError {\n status: resp.status,\n });\n }\n Ok(event_stream(resp.body))\n }\n}\n\n/// Build the normalized event stream from the raw SSE byte stream.\nfn event_stream(body: ByteStream) -> TurnStream {\n Box::pin(async_stream::stream! {\n let mut framer = SseFramer::new();\n let mut state = State::default();\n let mut body = body;\n while let Some(chunk) = body.next().await {\n let bytes = match chunk {\n Ok(b) => b,\n Err(_) => {\n yield Err(ProviderError::Transport);\n return;\n }\n };\n for sse in framer.push(&bytes) {\n match state.step(&sse) {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => {\n yield Err(e);\n return;\n }\n }\n }\n }\n if let Some(sse) = framer.finish() {\n match state.step(&sse) {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => {\n yield Err(e);\n return;\n }\n }\n }\n // Guarantee a terminal Completed even without a [DONE] marker.\n match state.finalize() {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => yield Err(e),\n }\n })\n}\n\n/// Streaming assembly state for one OpenAI-compatible turn.\n#[derive(Debug, Default)]\nstruct State {\n input_tokens: u64,\n output_tokens: u64,\n stop_reason: Option<StopReason>,\n tools: Vec<PartialTool>,\n flushed: bool,\n completed: bool,\n}\n\nimpl State {\n fn step(&mut self, sse: &SseEvent) -> Result<Vec<TurnEvent>, ProviderError> {\n let data = sse.data.trim();\n if data.is_empty() {\n return Ok(Vec::new());\n }\n if data == \"[DONE]\" {\n let mut out = self.flush_tools()?;\n if !self.completed {\n out.push(self.completed_event());\n self.completed = true;\n }\n return Ok(out);\n }\n\n let v: Value = serde_json::from_str(data).map_err(|_| ProviderError::MalformedResponse)?;\n let mut out = Vec::new();\n\n // A terminal usage-only chunk (choices empty) carries token counts.\n if let Some(usage) = v.get(\"usage\").filter(|u| !u.is_null()) {\n if let Some(i) = usage.get(\"prompt_tokens\").and_then(Value::as_u64) {\n self.input_tokens = i;\n }\n if let Some(o) = usage.get(\"completion_tokens\").and_then(Value::as_u64) {\n self.output_tokens = o;\n }\n }\n\n if let Some(choice) = v.pointer(\"/choices/0\") {\n if let Some(content) = choice.pointer(\"/delta/content\").and_then(Value::as_str) {\n if !content.is_empty() {\n out.push(TurnEvent::TextDelta(content.to_string()));\n }\n }\n if let Some(calls) = choice\n .pointer(\"/delta/tool_calls\")\n .and_then(Value::as_array)\n {\n for call in calls {\n self.accumulate_tool_call(call);\n }\n }\n if let Some(finish) = choice.get(\"finish_reason\").and_then(Value::as_str) {\n self.stop_reason = Some(StopReason::from_token(finish));\n // Tool calls are complete once a finish reason arrives; flush\n // them now (usage/Completed still comes at [DONE] or stream end).\n out.extend(self.flush_tools()?);\n }\n }\n\n Ok(out)\n }\n\n /// Merge one `tool_calls[]` fragment into the per-index accumulator.\n fn accumulate_tool_call(&mut self, call: &Value) {\n let idx = call.get(\"index\").and_then(Value::as_u64).unwrap_or(0) as usize;\n while self.tools.len() <= idx {\n self.tools.push(PartialTool::default());\n }\n let slot = &mut self.tools[idx];\n if let Some(id) = call.get(\"id\").and_then(Value::as_str) {\n if !id.is_empty() {\n slot.id = id.to_string();\n }\n }\n if let Some(name) = call.pointer(\"/function/name\").and_then(Value::as_str) {\n if !name.is_empty() {\n slot.name = name.to_string();\n }\n }\n if let Some(args) = call.pointer(\"/function/arguments\").and_then(Value::as_str) {\n slot.args.push_str(args);\n }\n }\n\n fn flush_tools(&mut self) -> Result<Vec<TurnEvent>, ProviderError> {\n if self.flushed || self.tools.is_empty() {\n return Ok(Vec::new());\n }\n self.flushed = true;\n let mut out = Vec::with_capacity(self.tools.len());\n for tool in std::mem::take(&mut self.tools) {\n out.push(finalize_tool(tool)?);\n }\n Ok(out)\n }\n\n fn finalize(&mut self) -> Result<Vec<TurnEvent>, ProviderError> {\n let mut out = self.flush_tools()?;\n if !self.completed {\n out.push(self.completed_event());\n self.completed = true;\n }\n Ok(out)\n }\n\n fn completed_event(&self) -> TurnEvent {\n TurnEvent::Completed(TurnCompletion {\n usage: TokenUsage {\n input_tokens: self.input_tokens,\n output_tokens: self.output_tokens,\n },\n stop_reason: self.stop_reason.clone().unwrap_or(StopReason::EndTurn),\n })\n }\n}\n"}, "407c3b78a8e737200fe5e3fdd9e2549275b479f6": {"t": 1785761010.0992415, "n": 11376, "h": "2de6d291ade690d8", "c": "//! The OpenAI-compatible chat/completions adapter.\n//!\n//! Covers any endpoint speaking the OpenAI chat/completions streaming protocol —\n//! DeepSeek, Kimi, Ollama, vLLM, and the like — with the endpoint configurable\n//! so a new provider is registerable with no code change (BR-6). The response is\n//! a stream of `data:` chunks terminated by `data: [DONE]`; `choices[0].delta`\n//! carries text (`content`) and tool-call fragments (`tool_calls[].function.\n//! arguments`, accumulated per index), and a final usage-only chunk carries\n//! `prompt_tokens` / `completion_tokens`. All of it normalizes to the same\n//! [`TurnEvent`]s the Anthropic adapter emits, in the same order.\n//!\n//! Because compat endpoints vary wildly in tool-call reliability, the default\n//! capability tier is conservative (`Degraded`, i.e. the reduced BR-6 harness);\n//! callers that trust a specific endpoint raise it explicitly.\n\nuse crate::capability::CapabilityProfile;\nuse crate::sse::{SseEvent, SseFramer};\nuse crate::transport::{ByteStream, HttpMethod, Transport, TransportRequest};\nuse crate::{\n finalize_tool, PartialTool, Provider, ProviderError, StopReason, TokenUsage, TurnCompletion,\n TurnEvent, TurnRequest, TurnStream,\n};\nuse async_trait::async_trait;\nuse futures::StreamExt;\nuse serde_json::{json, Value};\nuse teton_core::ToolCallTier;\n\n/// Configuration for an OpenAI-compatible provider instance.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct OpenAiCompatConfig {\n /// Stable provider id (feeds `provider_degraded`).\n pub id: String,\n /// Absolute chat/completions endpoint URL (e.g.\n /// `https://api.deepseek.com/v1/chat/completions`).\n pub endpoint: String,\n /// Capability profile. Defaults to the conservative `Degraded` tier for an\n /// unknown compat endpoint; raise it for a trusted provider.\n pub capabilities: CapabilityProfile,\n}\n\nimpl OpenAiCompatConfig {\n /// Create a config for `id` calling `endpoint`, with the conservative\n /// default capability profile (`Degraded` tier).\n #[must_use]\n pub fn new(id: impl Into<String>, endpoint: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n endpoint: endpoint.into(),\n capabilities: CapabilityProfile {\n tool_call_tier: ToolCallTier::Degraded,\n parallel_calls: false,\n max_context: 0,\n },\n }\n }\n\n /// Override the capability profile.\n #[must_use]\n pub fn with_capabilities(mut self, capabilities: CapabilityProfile) -> Self {\n self.capabilities = capabilities;\n self\n }\n}\n\n/// Adapter for any OpenAI-compatible chat/completions endpoint.\n#[derive(Debug, Clone)]\npub struct OpenAiCompatAdapter {\n config: OpenAiCompatConfig,\n}\n\nimpl OpenAiCompatAdapter {\n /// Create an adapter from a config.\n #[must_use]\n pub fn new(config: OpenAiCompatConfig) -> Self {\n Self { config }\n }\n\n fn build_request(&self, req: &TurnRequest) -> Result<TransportRequest, ProviderError> {\n let mut messages: Vec<Value> = Vec::with_capacity(req.messages.len() + 1);\n if let Some(system) = &req.system {\n messages.push(json!({\"role\": \"system\", \"content\": system}));\n }\n for m in &req.messages {\n messages.push(json!({\"role\": m.role.openai_str(), \"content\": m.content.as_str()}));\n }\n\n let mut body = json!({\n \"model\": req.model,\n \"messages\": messages,\n \"stream\": true,\n // Ask for usage in the terminal chunk so Completed always carries it\n // (BR-2).\n \"stream_options\": {\"include_usage\": true},\n \"max_tokens\": req.max_tokens,\n });\n if !req.tools.is_empty() {\n let tools: Vec<Value> = req\n .tools\n .iter()\n .map(|t| {\n json!({\n \"type\": \"function\",\n \"function\": {\n \"name\": t.name,\n \"description\": t.description,\n \"parameters\": t.input_schema,\n }\n })\n })\n .collect();\n body[\"tools\"] = json!(tools);\n }\n\n let body = serde_json::to_vec(&body).map_err(|e| ProviderError::Build(e.to_string()))?;\n Ok(TransportRequest {\n method: HttpMethod::Post,\n url: self.config.endpoint.clone(),\n // Auth header is added by the Transport (egress), not here (BR-7).\n headers: vec![\n (\"content-type\".to_string(), \"application/json\".to_string()),\n (\"accept\".to_string(), \"text/event-stream\".to_string()),\n ],\n body,\n })\n }\n}\n\n#[async_trait]\nimpl Provider for OpenAiCompatAdapter {\n fn id(&self) -> &str {\n &self.config.id\n }\n\n fn capabilities(&self) -> CapabilityProfile {\n self.config.capabilities\n }\n\n async fn stream_turn(\n &self,\n request: TurnRequest,\n transport: &dyn Transport,\n ) -> Result<TurnStream, ProviderError> {\n let http = self.build_request(&request)?;\n let resp = transport\n .execute(http)\n .await\n .map_err(ProviderError::from_transport)?;\n if resp.status >= 500 {\n return Err(ProviderError::ServerError {\n status: resp.status,\n });\n }\n if resp.status >= 400 {\n return Err(ProviderError::ClientError {\n status: resp.status,\n });\n }\n Ok(event_stream(resp.body))\n }\n}\n\n/// Build the normalized event stream from the raw SSE byte stream.\nfn event_stream(body: ByteStream) -> TurnStream {\n Box::pin(async_stream::stream! {\n let mut framer = SseFramer::new();\n let mut state = State::default();\n let mut body = body;\n while let Some(chunk) = body.next().await {\n let bytes = match chunk {\n Ok(b) => b,\n Err(_) => {\n yield Err(ProviderError::Transport);\n return;\n }\n };\n for sse in framer.push(&bytes) {\n match state.step(&sse) {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => {\n yield Err(e);\n return;\n }\n }\n }\n }\n if let Some(sse) = framer.finish() {\n match state.step(&sse) {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => {\n yield Err(e);\n return;\n }\n }\n }\n // Guarantee a terminal Completed even without a [DONE] marker.\n match state.finalize() {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => yield Err(e),\n }\n })\n}\n\n/// Streaming assembly state for one OpenAI-compatible turn.\n#[derive(Debug, Default)]\nstruct State {\n input_tokens: u64,\n output_tokens: u64,\n stop_reason: Option<StopReason>,\n tools: Vec<PartialTool>,\n flushed: bool,\n completed: bool,\n}\n\nimpl State {\n fn step(&mut self, sse: &SseEvent) -> Result<Vec<TurnEvent>, ProviderError> {\n let data = sse.data.trim();\n if data.is_empty() {\n return Ok(Vec::new());\n }\n if data == \"[DONE]\" {\n let mut out = self.flush_tools()?;\n if !self.completed {\n out.push(self.completed_event());\n self.completed = true;\n }\n return Ok(out);\n }\n\n let v: Value = serde_json::from_str(data).map_err(|_| ProviderError::MalformedResponse)?;\n let mut out = Vec::new();\n\n // A terminal usage-only chunk (choices empty) carries token counts.\n if let Some(usage) = v.get(\"usage\").filter(|u| !u.is_null()) {\n if let Some(i) = usage.get(\"prompt_tokens\").and_then(Value::as_u64) {\n self.input_tokens = i;\n }\n if let Some(o) = usage.get(\"completion_tokens\").and_then(Value::as_u64) {\n self.output_tokens = o;\n }\n }\n\n if let Some(choice) = v.pointer(\"/choices/0\") {\n if let Some(content) = choice.pointer(\"/delta/content\").and_then(Value::as_str) {\n if !content.is_empty() {\n out.push(TurnEvent::TextDelta(content.to_string()));\n }\n }\n if let Some(calls) = choice\n .pointer(\"/delta/tool_calls\")\n .and_then(Value::as_array)\n {\n for call in calls {\n self.accumulate_tool_call(call);\n }\n }\n if let Some(finish) = choice.get(\"finish_reason\").and_then(Value::as_str) {\n self.stop_reason = Some(StopReason::from_token(finish));\n // Tool calls are complete once a finish reason arrives; flush\n // them now (usage/Completed still comes at [DONE] or stream end).\n out.extend(self.flush_tools()?);\n }\n }\n\n Ok(out)\n }\n\n /// Merge one `tool_calls[]` fragment into the per-index accumulator.\n fn accumulate_tool_call(&mut self, call: &Value) {\n let idx = call.get(\"index\").and_then(Value::as_u64).unwrap_or(0) as usize;\n while self.tools.len() <= idx {\n self.tools.push(PartialTool::default());\n }\n let slot = &mut self.tools[idx];\n if let Some(id) = call.get(\"id\").and_then(Value::as_str) {\n if !id.is_empty() {\n slot.id = id.to_string();\n }\n }\n if let Some(name) = call.pointer(\"/function/name\").and_then(Value::as_str) {\n if !name.is_empty() {\n slot.name = name.to_string();\n }\n }\n if let Some(args) = call.pointer(\"/function/arguments\").and_then(Value::as_str) {\n slot.args.push_str(args);\n }\n }\n\n fn flush_tools(&mut self) -> Result<Vec<TurnEvent>, ProviderError> {\n if self.flushed || self.tools.is_empty() {\n return Ok(Vec::new());\n }\n self.flushed = true;\n let mut out = Vec::with_capacity(self.tools.len());\n for tool in std::mem::take(&mut self.tools) {\n out.push(finalize_tool(tool)?);\n }\n Ok(out)\n }\n\n fn finalize(&mut self) -> Result<Vec<TurnEvent>, ProviderError> {\n let mut out = self.flush_tools()?;\n if !self.completed {\n out.push(self.completed_event());\n self.completed = true;\n }\n Ok(out)\n }\n\n fn completed_event(&self) -> TurnEvent {\n TurnEvent::Completed(TurnCompletion {\n usage: TokenUsage {\n input_tokens: self.input_tokens,\n output_tokens: self.output_tokens,\n },\n stop_reason: self.stop_reason.clone().unwrap_or(StopReason::EndTurn),\n })\n }\n}\n"}, "115e4540beb119b8bbb10174277e08fe564f1ad6": {"t": 1786067649.4859028, "n": 11376, "h": "2de6d291ade690d8", "c": "//! The OpenAI-compatible chat/completions adapter.\n//!\n//! Covers any endpoint speaking the OpenAI chat/completions streaming protocol —\n//! DeepSeek, Kimi, Ollama, vLLM, and the like — with the endpoint configurable\n//! so a new provider is registerable with no code change (BR-6). The response is\n//! a stream of `data:` chunks terminated by `data: [DONE]`; `choices[0].delta`\n//! carries text (`content`) and tool-call fragments (`tool_calls[].function.\n//! arguments`, accumulated per index), and a final usage-only chunk carries\n//! `prompt_tokens` / `completion_tokens`. All of it normalizes to the same\n//! [`TurnEvent`]s the Anthropic adapter emits, in the same order.\n//!\n//! Because compat endpoints vary wildly in tool-call reliability, the default\n//! capability tier is conservative (`Degraded`, i.e. the reduced BR-6 harness);\n//! callers that trust a specific endpoint raise it explicitly.\n\nuse crate::capability::CapabilityProfile;\nuse crate::sse::{SseEvent, SseFramer};\nuse crate::transport::{ByteStream, HttpMethod, Transport, TransportRequest};\nuse crate::{\n finalize_tool, PartialTool, Provider, ProviderError, StopReason, TokenUsage, TurnCompletion,\n TurnEvent, TurnRequest, TurnStream,\n};\nuse async_trait::async_trait;\nuse futures::StreamExt;\nuse serde_json::{json, Value};\nuse teton_core::ToolCallTier;\n\n/// Configuration for an OpenAI-compatible provider instance.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct OpenAiCompatConfig {\n /// Stable provider id (feeds `provider_degraded`).\n pub id: String,\n /// Absolute chat/completions endpoint URL (e.g.\n /// `https://api.deepseek.com/v1/chat/completions`).\n pub endpoint: String,\n /// Capability profile. Defaults to the conservative `Degraded` tier for an\n /// unknown compat endpoint; raise it for a trusted provider.\n pub capabilities: CapabilityProfile,\n}\n\nimpl OpenAiCompatConfig {\n /// Create a config for `id` calling `endpoint`, with the conservative\n /// default capability profile (`Degraded` tier).\n #[must_use]\n pub fn new(id: impl Into<String>, endpoint: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n endpoint: endpoint.into(),\n capabilities: CapabilityProfile {\n tool_call_tier: ToolCallTier::Degraded,\n parallel_calls: false,\n max_context: 0,\n },\n }\n }\n\n /// Override the capability profile.\n #[must_use]\n pub fn with_capabilities(mut self, capabilities: CapabilityProfile) -> Self {\n self.capabilities = capabilities;\n self\n }\n}\n\n/// Adapter for any OpenAI-compatible chat/completions endpoint.\n#[derive(Debug, Clone)]\npub struct OpenAiCompatAdapter {\n config: OpenAiCompatConfig,\n}\n\nimpl OpenAiCompatAdapter {\n /// Create an adapter from a config.\n #[must_use]\n pub fn new(config: OpenAiCompatConfig) -> Self {\n Self { config }\n }\n\n fn build_request(&self, req: &TurnRequest) -> Result<TransportRequest, ProviderError> {\n let mut messages: Vec<Value> = Vec::with_capacity(req.messages.len() + 1);\n if let Some(system) = &req.system {\n messages.push(json!({\"role\": \"system\", \"content\": system}));\n }\n for m in &req.messages {\n messages.push(json!({\"role\": m.role.openai_str(), \"content\": m.content.as_str()}));\n }\n\n let mut body = json!({\n \"model\": req.model,\n \"messages\": messages,\n \"stream\": true,\n // Ask for usage in the terminal chunk so Completed always carries it\n // (BR-2).\n \"stream_options\": {\"include_usage\": true},\n \"max_tokens\": req.max_tokens,\n });\n if !req.tools.is_empty() {\n let tools: Vec<Value> = req\n .tools\n .iter()\n .map(|t| {\n json!({\n \"type\": \"function\",\n \"function\": {\n \"name\": t.name,\n \"description\": t.description,\n \"parameters\": t.input_schema,\n }\n })\n })\n .collect();\n body[\"tools\"] = json!(tools);\n }\n\n let body = serde_json::to_vec(&body).map_err(|e| ProviderError::Build(e.to_string()))?;\n Ok(TransportRequest {\n method: HttpMethod::Post,\n url: self.config.endpoint.clone(),\n // Auth header is added by the Transport (egress), not here (BR-7).\n headers: vec![\n (\"content-type\".to_string(), \"application/json\".to_string()),\n (\"accept\".to_string(), \"text/event-stream\".to_string()),\n ],\n body,\n })\n }\n}\n\n#[async_trait]\nimpl Provider for OpenAiCompatAdapter {\n fn id(&self) -> &str {\n &self.config.id\n }\n\n fn capabilities(&self) -> CapabilityProfile {\n self.config.capabilities\n }\n\n async fn stream_turn(\n &self,\n request: TurnRequest,\n transport: &dyn Transport,\n ) -> Result<TurnStream, ProviderError> {\n let http = self.build_request(&request)?;\n let resp = transport\n .execute(http)\n .await\n .map_err(ProviderError::from_transport)?;\n if resp.status >= 500 {\n return Err(ProviderError::ServerError {\n status: resp.status,\n });\n }\n if resp.status >= 400 {\n return Err(ProviderError::ClientError {\n status: resp.status,\n });\n }\n Ok(event_stream(resp.body))\n }\n}\n\n/// Build the normalized event stream from the raw SSE byte stream.\nfn event_stream(body: ByteStream) -> TurnStream {\n Box::pin(async_stream::stream! {\n let mut framer = SseFramer::new();\n let mut state = State::default();\n let mut body = body;\n while let Some(chunk) = body.next().await {\n let bytes = match chunk {\n Ok(b) => b,\n Err(_) => {\n yield Err(ProviderError::Transport);\n return;\n }\n };\n for sse in framer.push(&bytes) {\n match state.step(&sse) {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => {\n yield Err(e);\n return;\n }\n }\n }\n }\n if let Some(sse) = framer.finish() {\n match state.step(&sse) {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => {\n yield Err(e);\n return;\n }\n }\n }\n // Guarantee a terminal Completed even without a [DONE] marker.\n match state.finalize() {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => yield Err(e),\n }\n })\n}\n\n/// Streaming assembly state for one OpenAI-compatible turn.\n#[derive(Debug, Default)]\nstruct State {\n input_tokens: u64,\n output_tokens: u64,\n stop_reason: Option<StopReason>,\n tools: Vec<PartialTool>,\n flushed: bool,\n completed: bool,\n}\n\nimpl State {\n fn step(&mut self, sse: &SseEvent) -> Result<Vec<TurnEvent>, ProviderError> {\n let data = sse.data.trim();\n if data.is_empty() {\n return Ok(Vec::new());\n }\n if data == \"[DONE]\" {\n let mut out = self.flush_tools()?;\n if !self.completed {\n out.push(self.completed_event());\n self.completed = true;\n }\n return Ok(out);\n }\n\n let v: Value = serde_json::from_str(data).map_err(|_| ProviderError::MalformedResponse)?;\n let mut out = Vec::new();\n\n // A terminal usage-only chunk (choices empty) carries token counts.\n if let Some(usage) = v.get(\"usage\").filter(|u| !u.is_null()) {\n if let Some(i) = usage.get(\"prompt_tokens\").and_then(Value::as_u64) {\n self.input_tokens = i;\n }\n if let Some(o) = usage.get(\"completion_tokens\").and_then(Value::as_u64) {\n self.output_tokens = o;\n }\n }\n\n if let Some(choice) = v.pointer(\"/choices/0\") {\n if let Some(content) = choice.pointer(\"/delta/content\").and_then(Value::as_str) {\n if !content.is_empty() {\n out.push(TurnEvent::TextDelta(content.to_string()));\n }\n }\n if let Some(calls) = choice\n .pointer(\"/delta/tool_calls\")\n .and_then(Value::as_array)\n {\n for call in calls {\n self.accumulate_tool_call(call);\n }\n }\n if let Some(finish) = choice.get(\"finish_reason\").and_then(Value::as_str) {\n self.stop_reason = Some(StopReason::from_token(finish));\n // Tool calls are complete once a finish reason arrives; flush\n // them now (usage/Completed still comes at [DONE] or stream end).\n out.extend(self.flush_tools()?);\n }\n }\n\n Ok(out)\n }\n\n /// Merge one `tool_calls[]` fragment into the per-index accumulator.\n fn accumulate_tool_call(&mut self, call: &Value) {\n let idx = call.get(\"index\").and_then(Value::as_u64).unwrap_or(0) as usize;\n while self.tools.len() <= idx {\n self.tools.push(PartialTool::default());\n }\n let slot = &mut self.tools[idx];\n if let Some(id) = call.get(\"id\").and_then(Value::as_str) {\n if !id.is_empty() {\n slot.id = id.to_string();\n }\n }\n if let Some(name) = call.pointer(\"/function/name\").and_then(Value::as_str) {\n if !name.is_empty() {\n slot.name = name.to_string();\n }\n }\n if let Some(args) = call.pointer(\"/function/arguments\").and_then(Value::as_str) {\n slot.args.push_str(args);\n }\n }\n\n fn flush_tools(&mut self) -> Result<Vec<TurnEvent>, ProviderError> {\n if self.flushed || self.tools.is_empty() {\n return Ok(Vec::new());\n }\n self.flushed = true;\n let mut out = Vec::with_capacity(self.tools.len());\n for tool in std::mem::take(&mut self.tools) {\n out.push(finalize_tool(tool)?);\n }\n Ok(out)\n }\n\n fn finalize(&mut self) -> Result<Vec<TurnEvent>, ProviderError> {\n let mut out = self.flush_tools()?;\n if !self.completed {\n out.push(self.completed_event());\n self.completed = true;\n }\n Ok(out)\n }\n\n fn completed_event(&self) -> TurnEvent {\n TurnEvent::Completed(TurnCompletion {\n usage: TokenUsage {\n input_tokens: self.input_tokens,\n output_tokens: self.output_tokens,\n },\n stop_reason: self.stop_reason.clone().unwrap_or(StopReason::EndTurn),\n })\n }\n}\n"}, "55ece40e3325ad609adda1f69d79b6ec718a3af8": {"t": 1786068395.9019163, "n": 11376, "h": "2de6d291ade690d8", "c": "//! The OpenAI-compatible chat/completions adapter.\n//!\n//! Covers any endpoint speaking the OpenAI chat/completions streaming protocol —\n//! DeepSeek, Kimi, Ollama, vLLM, and the like — with the endpoint configurable\n//! so a new provider is registerable with no code change (BR-6). The response is\n//! a stream of `data:` chunks terminated by `data: [DONE]`; `choices[0].delta`\n//! carries text (`content`) and tool-call fragments (`tool_calls[].function.\n//! arguments`, accumulated per index), and a final usage-only chunk carries\n//! `prompt_tokens` / `completion_tokens`. All of it normalizes to the same\n//! [`TurnEvent`]s the Anthropic adapter emits, in the same order.\n//!\n//! Because compat endpoints vary wildly in tool-call reliability, the default\n//! capability tier is conservative (`Degraded`, i.e. the reduced BR-6 harness);\n//! callers that trust a specific endpoint raise it explicitly.\n\nuse crate::capability::CapabilityProfile;\nuse crate::sse::{SseEvent, SseFramer};\nuse crate::transport::{ByteStream, HttpMethod, Transport, TransportRequest};\nuse crate::{\n finalize_tool, PartialTool, Provider, ProviderError, StopReason, TokenUsage, TurnCompletion,\n TurnEvent, TurnRequest, TurnStream,\n};\nuse async_trait::async_trait;\nuse futures::StreamExt;\nuse serde_json::{json, Value};\nuse teton_core::ToolCallTier;\n\n/// Configuration for an OpenAI-compatible provider instance.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct OpenAiCompatConfig {\n /// Stable provider id (feeds `provider_degraded`).\n pub id: String,\n /// Absolute chat/completions endpoint URL (e.g.\n /// `https://api.deepseek.com/v1/chat/completions`).\n pub endpoint: String,\n /// Capability profile. Defaults to the conservative `Degraded` tier for an\n /// unknown compat endpoint; raise it for a trusted provider.\n pub capabilities: CapabilityProfile,\n}\n\nimpl OpenAiCompatConfig {\n /// Create a config for `id` calling `endpoint`, with the conservative\n /// default capability profile (`Degraded` tier).\n #[must_use]\n pub fn new(id: impl Into<String>, endpoint: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n endpoint: endpoint.into(),\n capabilities: CapabilityProfile {\n tool_call_tier: ToolCallTier::Degraded,\n parallel_calls: false,\n max_context: 0,\n },\n }\n }\n\n /// Override the capability profile.\n #[must_use]\n pub fn with_capabilities(mut self, capabilities: CapabilityProfile) -> Self {\n self.capabilities = capabilities;\n self\n }\n}\n\n/// Adapter for any OpenAI-compatible chat/completions endpoint.\n#[derive(Debug, Clone)]\npub struct OpenAiCompatAdapter {\n config: OpenAiCompatConfig,\n}\n\nimpl OpenAiCompatAdapter {\n /// Create an adapter from a config.\n #[must_use]\n pub fn new(config: OpenAiCompatConfig) -> Self {\n Self { config }\n }\n\n fn build_request(&self, req: &TurnRequest) -> Result<TransportRequest, ProviderError> {\n let mut messages: Vec<Value> = Vec::with_capacity(req.messages.len() + 1);\n if let Some(system) = &req.system {\n messages.push(json!({\"role\": \"system\", \"content\": system}));\n }\n for m in &req.messages {\n messages.push(json!({\"role\": m.role.openai_str(), \"content\": m.content.as_str()}));\n }\n\n let mut body = json!({\n \"model\": req.model,\n \"messages\": messages,\n \"stream\": true,\n // Ask for usage in the terminal chunk so Completed always carries it\n // (BR-2).\n \"stream_options\": {\"include_usage\": true},\n \"max_tokens\": req.max_tokens,\n });\n if !req.tools.is_empty() {\n let tools: Vec<Value> = req\n .tools\n .iter()\n .map(|t| {\n json!({\n \"type\": \"function\",\n \"function\": {\n \"name\": t.name,\n \"description\": t.description,\n \"parameters\": t.input_schema,\n }\n })\n })\n .collect();\n body[\"tools\"] = json!(tools);\n }\n\n let body = serde_json::to_vec(&body).map_err(|e| ProviderError::Build(e.to_string()))?;\n Ok(TransportRequest {\n method: HttpMethod::Post,\n url: self.config.endpoint.clone(),\n // Auth header is added by the Transport (egress), not here (BR-7).\n headers: vec![\n (\"content-type\".to_string(), \"application/json\".to_string()),\n (\"accept\".to_string(), \"text/event-stream\".to_string()),\n ],\n body,\n })\n }\n}\n\n#[async_trait]\nimpl Provider for OpenAiCompatAdapter {\n fn id(&self) -> &str {\n &self.config.id\n }\n\n fn capabilities(&self) -> CapabilityProfile {\n self.config.capabilities\n }\n\n async fn stream_turn(\n &self,\n request: TurnRequest,\n transport: &dyn Transport,\n ) -> Result<TurnStream, ProviderError> {\n let http = self.build_request(&request)?;\n let resp = transport\n .execute(http)\n .await\n .map_err(ProviderError::from_transport)?;\n if resp.status >= 500 {\n return Err(ProviderError::ServerError {\n status: resp.status,\n });\n }\n if resp.status >= 400 {\n return Err(ProviderError::ClientError {\n status: resp.status,\n });\n }\n Ok(event_stream(resp.body))\n }\n}\n\n/// Build the normalized event stream from the raw SSE byte stream.\nfn event_stream(body: ByteStream) -> TurnStream {\n Box::pin(async_stream::stream! {\n let mut framer = SseFramer::new();\n let mut state = State::default();\n let mut body = body;\n while let Some(chunk) = body.next().await {\n let bytes = match chunk {\n Ok(b) => b,\n Err(_) => {\n yield Err(ProviderError::Transport);\n return;\n }\n };\n for sse in framer.push(&bytes) {\n match state.step(&sse) {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => {\n yield Err(e);\n return;\n }\n }\n }\n }\n if let Some(sse) = framer.finish() {\n match state.step(&sse) {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => {\n yield Err(e);\n return;\n }\n }\n }\n // Guarantee a terminal Completed even without a [DONE] marker.\n match state.finalize() {\n Ok(events) => {\n for e in events {\n yield Ok(e);\n }\n }\n Err(e) => yield Err(e),\n }\n })\n}\n\n/// Streaming assembly state for one OpenAI-compatible turn.\n#[derive(Debug, Default)]\nstruct State {\n input_tokens: u64,\n output_tokens: u64,\n stop_reason: Option<StopReason>,\n tools: Vec<PartialTool>,\n flushed: bool,\n completed: bool,\n}\n\nimpl State {\n fn step(&mut self, sse: &SseEvent) -> Result<Vec<TurnEvent>, ProviderError> {\n let data = sse.data.trim();\n if data.is_empty() {\n return Ok(Vec::new());\n }\n if data == \"[DONE]\" {\n let mut out = self.flush_tools()?;\n if !self.completed {\n out.push(self.completed_event());\n self.completed = true;\n }\n return Ok(out);\n }\n\n let v: Value = serde_json::from_str(data).map_err(|_| ProviderError::MalformedResponse)?;\n let mut out = Vec::new();\n\n // A terminal usage-only chunk (choices empty) carries token counts.\n if let Some(usage) = v.get(\"usage\").filter(|u| !u.is_null()) {\n if let Some(i) = usage.get(\"prompt_tokens\").and_then(Value::as_u64) {\n self.input_tokens = i;\n }\n if let Some(o) = usage.get(\"completion_tokens\").and_then(Value::as_u64) {\n self.output_tokens = o;\n }\n }\n\n if let Some(choice) = v.pointer(\"/choices/0\") {\n if let Some(content) = choice.pointer(\"/delta/content\").and_then(Value::as_str) {\n if !content.is_empty() {\n out.push(TurnEvent::TextDelta(content.to_string()));\n }\n }\n if let Some(calls) = choice\n .pointer(\"/delta/tool_calls\")\n .and_then(Value::as_array)\n {\n for call in calls {\n self.accumulate_tool_call(call);\n }\n }\n if let Some(finish) = choice.get(\"finish_reason\").and_then(Value::as_str) {\n self.stop_reason = Some(StopReason::from_token(finish));\n // Tool calls are complete once a finish reason arrives; flush\n // them now (usage/Completed still comes at [DONE] or stream end).\n out.extend(self.flush_tools()?);\n }\n }\n\n Ok(out)\n }\n\n /// Merge one `tool_calls[]` fragment into the per-index accumulator.\n fn accumulate_tool_call(&mut self, call: &Value) {\n let idx = call.get(\"index\").and_then(Value::as_u64).unwrap_or(0) as usize;\n while self.tools.len() <= idx {\n self.tools.push(PartialTool::default());\n }\n let slot = &mut self.tools[idx];\n if let Some(id) = call.get(\"id\").and_then(Value::as_str) {\n if !id.is_empty() {\n slot.id = id.to_string();\n }\n }\n if let Some(name) = call.pointer(\"/function/name\").and_then(Value::as_str) {\n if !name.is_empty() {\n slot.name = name.to_string();\n }\n }\n if let Some(args) = call.pointer(\"/function/arguments\").and_then(Value::as_str) {\n slot.args.push_str(args);\n }\n }\n\n fn flush_tools(&mut self) -> Result<Vec<TurnEvent>, ProviderError> {\n if self.flushed || self.tools.is_empty() {\n return Ok(Vec::new());\n }\n self.flushed = true;\n let mut out = Vec::with_capacity(self.tools.len());\n for tool in std::mem::take(&mut self.tools) {\n out.push(finalize_tool(tool)?);\n }\n Ok(out)\n }\n\n fn finalize(&mut self) -> Result<Vec<TurnEvent>, ProviderError> {\n let mut out = self.flush_tools()?;\n if !self.completed {\n out.push(self.completed_event());\n self.completed = true;\n }\n Ok(out)\n }\n\n fn completed_event(&self) -> TurnEvent {\n TurnEvent::Completed(TurnCompletion {\n usage: TokenUsage {\n input_tokens: self.input_tokens,\n output_tokens: self.output_tokens,\n },\n stop_reason: self.stop_reason.clone().unwrap_or(StopReason::EndTurn),\n })\n }\n}\n"}}}} |