mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
Closes #3612
This commit is contained in:
committed by
GitHub
parent
60e38a71d1
commit
3ce157d22e
@@ -37,6 +37,7 @@
|
||||
- **fix(reasoning): replay `reasoning_content` on plain DeepSeek turns** ([#3632] — thanks @adivekar-utexas): the reasoning-replay gate previously only fired when an assistant message already carried `reasoning_content`. Plain (non-tool-call) turns whose `reasoning_content` was stripped by the client (e.g. Cursor) were forwarded without it, so DeepSeek V4+ rejected the request with 400 "the reasoning_content in the thinking mode must be passed back". The gate now also covers missing/empty `reasoning_content` on DeepSeek replay targets, injecting the cached reasoning (or the non-Anthropic placeholder) so multi-turn text conversations no longer 400. Fixes #1682. 2 regression tests.
|
||||
- **fix(kiro): route enterprise IAM Identity Center accounts to their regional endpoint** ([#3631] — thanks @artickc): Kiro/CodeWhisperer access tokens and Q Developer profile ARNs are region-bound, so enterprise IAM Identity Center accounts outside `us-east-1` (e.g. `eu-central-1`) were rejected by the default host. Adds `resolveKiroRegion` (stored region → profileArn region → `us-east-1`) and `kiroRuntimeHost` (regional `q.{region}.amazonaws.com`, legacy `codewhisperer.us-east-1` for the default), routes chat + usage to the regional endpoint, and discovers the region-matched `profileArn` via `ListAvailableProfiles` in a best-effort `postExchange` hook. 9 tests.
|
||||
- **fix(combo): skip same-provider/connection targets on connection-level errors** ([#3637] — thanks @herjarsa): on connection-level upstream errors (408/500/502/503/504/524), remaining same-`provider:connection` targets in a combo request are now skipped to avoid hammering a known-bad connection, in both the priority and round-robin paths. Adjusted in review to **exclude OmniRoute circuit-breaker-open responses** (503 + `X-OmniRoute-Provider-Breaker` / `provider_circuit_open`) from this skip, preserving the invariant that a breaker-open is an ordinary target failure (the next same-provider target is still tried). Co-authored with @herjarsa.
|
||||
- **/v1/responses**: detect stream readiness for tool-call-only and `object`-less chunks so Codex-shaped (reasoning + tools) requests no longer fail with "Stream ended before producing useful content" (#3612)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -104,9 +104,13 @@ function hasOpenAIResponseLifecyclePayload(
|
||||
}
|
||||
|
||||
function hasChatCompletionToolCallStart(value: unknown): boolean {
|
||||
const hasToolCallId = (item: unknown) => isRecord(item) && hasNonEmptyString(item.id);
|
||||
if (Array.isArray(value)) return value.some(hasToolCallId);
|
||||
return hasToolCallId(value);
|
||||
// Accept a tool_call item if it has a non-empty id (fully-formed chunk) OR if it
|
||||
// carries a numeric index (first OpenAI streaming chunk — id/name arrive in later
|
||||
// chunks). Either form signals that a tool call has started. (#3612)
|
||||
const hasToolCallStart = (item: unknown) =>
|
||||
isRecord(item) && (hasNonEmptyString(item.id) || typeof item.index === "number");
|
||||
if (Array.isArray(value)) return value.some(hasToolCallStart);
|
||||
return hasToolCallStart(value);
|
||||
}
|
||||
|
||||
function hasChatCompletionFunctionCallStart(value: unknown): boolean {
|
||||
@@ -114,9 +118,15 @@ function hasChatCompletionFunctionCallStart(value: unknown): boolean {
|
||||
}
|
||||
|
||||
function hasChatCompletionChunkStartPayload(payload: Record<string, unknown>): boolean {
|
||||
if (payload.object !== "chat.completion.chunk" && payload.type !== "chat.completion.chunk") {
|
||||
return false;
|
||||
}
|
||||
// If object or type is PRESENT and is clearly a non-chat-chunk type, reject early.
|
||||
// If both are absent (many OA-compatible backends omit object), we fall through and
|
||||
// let the choices structure decide. (#3612)
|
||||
const obj = payload.object;
|
||||
const typ = payload.type;
|
||||
const hasForeignObject =
|
||||
(typeof obj === "string" && obj !== "chat.completion.chunk") ||
|
||||
(typeof typ === "string" && typ !== "chat.completion.chunk");
|
||||
if (hasForeignObject) return false;
|
||||
|
||||
const choices = payload.choices;
|
||||
if (!Array.isArray(choices) || choices.length === 0) return false;
|
||||
|
||||
@@ -327,6 +327,8 @@ test("hasStreamReadinessSignal accepts structural chat completion chunk starts",
|
||||
),
|
||||
false
|
||||
);
|
||||
// #3612: index-only tool_call chunk (first chunk in OpenAI streaming — no id yet)
|
||||
// MUST be treated as a readiness signal (tool-call has started)
|
||||
assert.equal(
|
||||
hasStreamReadinessSignal(
|
||||
`data: ${JSON.stringify({
|
||||
@@ -334,7 +336,7 @@ test("hasStreamReadinessSignal accepts structural chat completion chunk starts",
|
||||
choices: [{ index: 0, delta: { tool_calls: [{ index: 0 }] } }],
|
||||
})}\n\n`
|
||||
),
|
||||
false
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
hasStreamReadinessSignal(
|
||||
@@ -345,6 +347,27 @@ test("hasStreamReadinessSignal accepts structural chat completion chunk starts",
|
||||
),
|
||||
false
|
||||
);
|
||||
// #3612: chunk with valid choices but NO object/type field (some OA-compatible backends
|
||||
// omit object) — must be accepted as a readiness signal when delta.role is present
|
||||
assert.equal(
|
||||
hasStreamReadinessSignal(
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl-xyz",
|
||||
choices: [{ index: 0, delta: { role: "assistant" } }],
|
||||
})}\n\n`
|
||||
),
|
||||
true
|
||||
);
|
||||
// object present but a different (non-chat-chunk) type must still be rejected
|
||||
assert.equal(
|
||||
hasStreamReadinessSignal(
|
||||
`data: ${JSON.stringify({
|
||||
object: "chat.completion",
|
||||
choices: [{ index: 0, delta: { role: "assistant" } }],
|
||||
})}\n\n`
|
||||
),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("ensureStreamReadiness preserves buffered chunks when stream starts", async () => {
|
||||
|
||||
Reference in New Issue
Block a user