mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 16:42:16 +03:00
Retargetado para release/v3.8.51 (release/v3.8.50 está congelada — freeze issue #11439). Validado em lote combinado (batch-0824h2, junto de #11435/#11436/#11437) contra o tip de release/v3.8.51: typecheck:core limpo, gates estáticos OK, 127/127 testes focados passando. Investigação sólida com repro real via container isolado, três causas independentes identificadas e corrigidas com testes de regressão dedicados para cada uma. Obrigado pela contribuição!
94 lines
4.5 KiB
TypeScript
94 lines
4.5 KiB
TypeScript
/**
|
|
* responsesContinuationStore.ts — OmniRoute-native `previous_response_id`
|
|
* virtualization for the OpenAI Responses API.
|
|
*
|
|
* Exposes `previous_response_id` continuation to clients unconditionally,
|
|
* regardless of whether the actual upstream provider for a connection
|
|
* supports Responses-API state at all: OmniRoute resolves the response id
|
|
* back to the full input/output it produced and reconstructs the full
|
|
* request server-side before forwarding upstream (full history, exactly as
|
|
* today) -- the client only ever has to resend the new delta.
|
|
*
|
|
* Storage: reuses the existing call-log pipeline artifact (full, untruncated
|
|
* request/response payloads, already gated by `call_log_pipeline_enabled`
|
|
* and already retained/cleaned up by the existing call-log lifecycle)
|
|
* instead of duplicating conversation content into a second store. Only a
|
|
* lightweight `call_logs.response_id` index (154_call_logs_response_id.sql)
|
|
* is new. Every lookup is scoped by `api_key_id` -- one client can never
|
|
* resolve another client's stored conversation.
|
|
*/
|
|
|
|
import { getDbInstance } from "./core";
|
|
import { readCallArtifact } from "../usage/callLogArtifacts";
|
|
|
|
export type ResponsesContinuationState = {
|
|
input: unknown[];
|
|
output: unknown[];
|
|
};
|
|
|
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
/**
|
|
* Resolve the full input + output a prior Responses API call produced, so
|
|
* the caller can reconstruct `full_input = stored.input + stored.output +
|
|
* new_delta`. Returns null on any lookup/read/shape failure (unknown id,
|
|
* wrong tenant, artifact missing, or an artifact whose pipeline payload was
|
|
* size-limit-omitted -- see MAX_CALL_LOG_ARTIFACT_BYTES in
|
|
* callLogArtifacts.ts) so the caller can fail closed and ask the client to
|
|
* resend full history, exactly like a real `previous_response_not_found`
|
|
* from OpenAI itself.
|
|
*/
|
|
export function resolvePreviousResponseState(
|
|
responseId: string,
|
|
apiKeyId: string | null | undefined
|
|
): ResponsesContinuationState | null {
|
|
if (!responseId) return null;
|
|
|
|
const db = getDbInstance();
|
|
const row = db
|
|
.prepare(
|
|
`SELECT artifact_relpath, api_key_id FROM call_logs
|
|
WHERE response_id = ? AND detail_state = 'ready'
|
|
ORDER BY timestamp DESC LIMIT 1`
|
|
)
|
|
.get(responseId) as { artifact_relpath: string | null; api_key_id: string | null } | undefined;
|
|
|
|
if (!row || !row.artifact_relpath) return null;
|
|
// Tenant isolation: a response id is only ever handed back to the API key
|
|
// that created it. A stored row with no api_key_id at all (no-log/legacy)
|
|
// can never be resolved by any key -- fail closed rather than guess.
|
|
if (!apiKeyId || row.api_key_id !== apiKeyId) return null;
|
|
|
|
const { artifact, state } = readCallArtifact(row.artifact_relpath);
|
|
if (state !== "ready" || !artifact?.pipeline) return null;
|
|
|
|
const clientRawRequest = artifact.pipeline.clientRawRequest as { body?: unknown } | undefined;
|
|
const clientResponse = artifact.pipeline.clientResponse as
|
|
{ output?: unknown; summary?: { output?: unknown } } | undefined;
|
|
|
|
// clientRawRequest, not providerRequest: this store only ever fires for
|
|
// sourceFormat === OPENAI_RESPONSES (see chat.ts), so the client's own
|
|
// request is always Responses-API shaped and always carries `input`.
|
|
// providerRequest is upstream-shaped and only has `input` for a native
|
|
// passthrough Responses API upstream -- any translated upstream (e.g. Chat
|
|
// Completions `messages`) rewrites the wire body entirely, which made this
|
|
// unconditionally unresolvable for every translate-mode/auto-routed
|
|
// connection (previous_response_not_found on every attempt, regardless of
|
|
// whether the id was real and the artifact was otherwise 'ready').
|
|
const input = isPlainRecord(clientRawRequest?.body) ? clientRawRequest.body.input : undefined;
|
|
// A streaming clientResponse is clientPayloadCollector.build()'s output, which
|
|
// always nests the caller's summary under `.summary` (see
|
|
// createStructuredSSECollector in streamPayloadCollector.ts) -- a non-streaming
|
|
// one carries `output` directly. Same dual-shape concern as extractResponsesId
|
|
// in open-sse/handlers/chatCore/attemptLogging.ts, checked here independently
|
|
// since this reads back a stored artifact rather than the live object.
|
|
const output = Array.isArray(clientResponse?.output)
|
|
? clientResponse.output
|
|
: clientResponse?.summary?.output;
|
|
if (!Array.isArray(input) || !Array.isArray(output)) return null;
|
|
|
|
return { input, output };
|
|
}
|