Repository navigation
fix(inference): actionable error for completion-only model 404 + unmistakable run_code failure (#3193) #3211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -129,6 +129,43 @@ impl OpenAiCompatibleProvider { | |
| } | ||
| } | ||
|
|
||
| /// Build an actionable error for a completion-only model that was routed | ||
| /// to `/v1/chat/completions`. OpenHuman only speaks the chat-completions | ||
| /// API (with an optional `/v1/responses` fallback) — a completion-only / | ||
| /// base model 404s here and the responses fallback cannot rescue it, so we | ||
| /// surface the model name and concrete remediation instead of an opaque | ||
| /// "responses fallback failed" chain. See issue #3193. | ||
| fn completion_only_model_message(&self, model: &str, sanitized: &str) -> String { | ||
| format!( | ||
| "{name} API error (404): model '{model}' does not support the \ | ||
| chat-completions API that OpenHuman uses — it appears to be a \ | ||
| completion-only / base model. Assign a chat-capable model to this \ | ||
| provider (e.g. in Settings → AI), or pick a different model. \ | ||
| Provider detail: {sanitized}", | ||
| name = self.name, | ||
| ) | ||
| } | ||
|
|
||
| /// Guard shared by every chat-completions 404 handler: if the body shows a | ||
| /// completion-only model, return the actionable error so the caller can | ||
| /// fail fast instead of attempting the futile `/v1/responses` fallback. | ||
| /// `None` means "not this case — proceed with normal fallback/enrich". | ||
| /// See issue #3193. | ||
| fn completion_only_404_guard( | ||
| &self, | ||
| status: reqwest::StatusCode, | ||
| sanitized: &str, | ||
| model: &str, | ||
| ) -> Option<anyhow::Error> { | ||
| if Self::is_completion_only_model_404(status, sanitized) { | ||
| Some(anyhow::anyhow!( | ||
| self.completion_only_model_message(model, sanitized) | ||
| )) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
|
|
||
| /// Create a provider with a custom User-Agent header. | ||
| /// | ||
| /// Some providers (for example Kimi Code) require a specific User-Agent | ||
|
|
@@ -798,6 +835,24 @@ impl OpenAiCompatibleProvider { | |
| Self::is_native_tool_schema_unsupported(reqwest::StatusCode::BAD_REQUEST, error) | ||
| } | ||
|
|
||
| /// Detect a 404 whose body says the model is completion-only and cannot be | ||
| /// served from `/v1/chat/completions` (OpenAI: "This is not a chat model | ||
| /// and thus not supported in the v1/chat/completions endpoint. Did you | ||
| /// mean to use v1/completions?"). When this fires, attempting the | ||
| /// `/v1/responses` fallback is futile, so callers should fail fast with an | ||
| /// actionable message via [`completion_only_model_message`]. The match is | ||
| /// deliberately tight so ordinary "model does not exist" 404s are NOT | ||
| /// caught (those should keep their existing fallback / enrich behaviour). | ||
| /// See issue #3193. | ||
| fn is_completion_only_model_404(status: reqwest::StatusCode, error: &str) -> bool { | ||
| if status != reqwest::StatusCode::NOT_FOUND { | ||
| return false; | ||
| } | ||
| let lower = error.to_lowercase(); | ||
| lower.contains("not a chat model") | ||
| || (lower.contains("v1/chat/completions") && lower.contains("v1/completions")) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Second disjunct is correct today but fragile to OpenAI rewording. The string Suggest a one-line comment on line 853 making the "two independent substrings" intent explicit, e.g.: // Defensive fallback: OpenAI's current phrasing references BOTH endpoint paths
// as two separate substrings (`v1/chat/completions` does not contain `v1/completions`). |
||
| } | ||
|
|
||
| /// Streaming variant of the native-tools chat path. | ||
| /// | ||
| /// Sends the request with `stream: true`, consumes the upstream SSE | ||
|
|
@@ -1343,6 +1398,12 @@ impl Provider for OpenAiCompatibleProvider { | |
| let error = response.text().await?; | ||
| let sanitized = super::sanitize_api_error(&error); | ||
|
|
||
| // A completion-only model 404s here and the /v1/responses fallback | ||
| // cannot rescue it — fail fast with actionable guidance (#3193). | ||
| if let Some(err) = self.completion_only_404_guard(status, &sanitized, model) { | ||
| return Err(err); | ||
| } | ||
|
|
||
| if status == reqwest::StatusCode::NOT_FOUND && self.supports_responses_fallback { | ||
| return self | ||
| .chat_via_responses(credential, &fallback_messages, model) | ||
|
|
@@ -1510,18 +1571,38 @@ impl Provider for OpenAiCompatibleProvider { | |
| if !response.status().is_success() { | ||
| let status = response.status(); | ||
|
|
||
| // Mirror chat_with_system: 404 may mean this provider uses the Responses API | ||
| if status == reqwest::StatusCode::NOT_FOUND && self.supports_responses_fallback { | ||
| return self | ||
| .chat_via_responses(credential, &effective_messages, model) | ||
| .await | ||
| .map_err(|responses_err| { | ||
| let fb = super::format_anyhow_chain(&responses_err); | ||
| anyhow::anyhow!( | ||
| "{} API error (chat completions unavailable; responses fallback failed: {fb})", | ||
| self.name | ||
| ) | ||
| }); | ||
| // A 404 may mean this provider uses the Responses API, OR that the | ||
| // model is completion-only. Read the body once so we can tell the | ||
| // two apart (#3193) — only the 404 branch needs it; the response is | ||
| // not used again here, so `api_error` below still owns the rest. | ||
| if status == reqwest::StatusCode::NOT_FOUND { | ||
| let error = response.text().await?; | ||
| let sanitized = super::sanitize_api_error(&error); | ||
|
|
||
| // Completion-only model: the responses fallback can't help — | ||
| // fail fast with actionable guidance. | ||
| if let Some(err) = self.completion_only_404_guard(status, &sanitized, model) { | ||
| return Err(err); | ||
| } | ||
|
|
||
| if self.supports_responses_fallback { | ||
| return self | ||
| .chat_via_responses(credential, &effective_messages, model) | ||
| .await | ||
| .map_err(|responses_err| { | ||
| let fb = super::format_anyhow_chain(&responses_err); | ||
| anyhow::anyhow!( | ||
| "{} API error ({status}): {sanitized} (chat completions unavailable; responses fallback failed: {fb})", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Undocumented behavior change on the non-completion-only fallback path. Before this PR, After this PR it reads: That's a strict improvement (now includes the original 404 body and status), but it's a behavior change to a path unrelated to completion-only detection — any ordinary 404 that triggers the responses fallback will now have a different error string. The PR body's Behavior Changes section only lists the completion-only and subagent-envelope changes, not this. Suggest adding a one-liner there so downstream log parsers / Sentry classifiers aren't surprised. (Cross-reference: the same widening was applied at the native-tool-calling |
||
| self.name | ||
| ) | ||
| }); | ||
| } | ||
|
|
||
| let enriched = self.enrich_404_message( | ||
| format!("{} API error ({status}): {sanitized}", self.name), | ||
| status, | ||
| ); | ||
| return Err(anyhow::anyhow!("{enriched}")); | ||
| } | ||
|
|
||
| let err = super::api_error(&self.name, response).await; | ||
|
|
@@ -1859,6 +1940,12 @@ impl Provider for OpenAiCompatibleProvider { | |
| }); | ||
| } | ||
|
|
||
| // A completion-only model 404s here and the /v1/responses fallback | ||
| // cannot rescue it — fail fast with actionable guidance (#3193). | ||
| if let Some(err) = self.completion_only_404_guard(status, &sanitized, model) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adjacent code paths share the same 404 surface but skip the new guard. The PR body claims "completion-only detection is shared across all three chat entrypoints", but two more entrypoints on this same provider can hit the same OpenAI completion-only 404:
Proposal: either replicate the 3-line guard at all four additional sites (preferred — they're 1-liners and the helper is already shared), or document in the PR body why streaming + |
||
| return Err(err); | ||
| } | ||
|
|
||
| if status == reqwest::StatusCode::NOT_FOUND && self.supports_responses_fallback { | ||
| return self | ||
| .chat_via_responses(credential, &effective_messages, model) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In-band prompt-engineering preamble now applied to every subagent failure.
format_subagent_failureis invoked on the genericrun_subagenterror path — so timeouts, budget exhaustion, transport errors, and any other transient failure now also carry the ~200-char "do NOT treat this as success or fabricate an output" preamble in transcripts, logs, and observability. That's intentional given #3193's "hallucinated success" symptom, but it's a behavior change broader than the issue title suggests ("completion-only 404"). Worth either:Not blocking — flagging so it's a deliberate decision.