harness-providers: OpenAI chat + responses codecs, provider, registry wiring (M3)

- codec/openai_chat.rs: /chat/completions request builder (flat messages,
  tool_calls, system → leading system message, reasoning_effort) + SSE decoder
  (content/tool_calls-by-index/reasoning_content accumulation, [DONE] sentinel,
  include_usage). Also the fallback codec for OpenAI-compatible endpoints.
- codec/openai_responses.rs: /responses request builder (input items, tool calls
  and results as top-level function_call/function_call_output, instructions,
  reasoning {effort, summary}) + SSE decoder (output_item add/done, output_text
  and reasoning_summary deltas, function_call_arguments, response.completed usage
  with separate reasoning_tokens).
- openai.rs: OpenAiProvider routing gpt-*/o-* → responses, else chat; Bearer auth;
  error classification incl. 400 context_length_exceeded → ContextOverflow.
- Registered in harness-app from config.providers["openai"] (api_key + optional
  base_url), sourced from OPENAI_API_KEY by the existing config loader.
- 21 codec/provider unit tests (decode fixtures + request-builder assertions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 23:35:49 +02:00
co-authored by Claude Opus 4.8
parent 0812f6d94c
commit e2706a1560
6 changed files with 1097 additions and 1 deletions
+16 -1
View File
@@ -19,7 +19,7 @@ use harness_core::tool::ToolRegistry;
use harness_core::types::{
Message, MessageId, ModelRef, Part, PartBody, PartId, Session, SessionId,
};
use harness_providers::{AnthropicProvider, ProviderRegistry};
use harness_providers::{AnthropicProvider, OpenAiProvider, ProviderRegistry};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
@@ -122,6 +122,21 @@ impl EngineHandle {
{
providers.register(Arc::new(AnthropicProvider::new(key)));
}
if let Some(key) = config
.providers
.get("openai")
.and_then(|p| p.api_key.clone())
{
let provider = match config
.providers
.get("openai")
.and_then(|p| p.base_url.clone())
{
Some(base_url) => OpenAiProvider::with_base_url(key, base_url),
None => OpenAiProvider::new(key),
};
providers.register(Arc::new(provider));
}
let data_dir = dirs::data_dir()
.unwrap_or_else(std::env::temp_dir)