mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 12:22:34 +03:00
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, e a suíte vitest:ui completa (2149) verde. Sobre esta PR especificamente: rodei os **23 arquivos de teste** que ela toca sobre o tip final, depois do merge da base — **392/392**. A migration `174_server_tool_executions.sql` não colide (o tip está em 173, e você já a renumerou em `c35f0fd7`). O dono foi consultado antes do merge, porque o loop está atrás da flag `SERVER_OWNED_TOOL_LOOP_ENABLED` mas o primeiro send não-streaming mudou de dono sem flag, e a verificação manual em combo com Memory continuava desmarcada. A condição dele foi: entra se os testes focados passarem aqui. Passaram. O lock de passthrough (`fetchCalls.length === 1`) é a parte que mais me convenceu — o double-dispatch que um `if (stream)` em volta do send existente causaria é exatamente o tipo de regressão que não aparece em teste de comportamento, só em contagem de chamada. **Três ajustes meus na sua branch:** 1. `tests/unit/chatcore-stream-error-result.test.ts` procurava `"const legResult = await runNonStreamingProviderLeg"`, mas o seu commit final `6077b9dd` passou a reatribuir `legResult` e trocou para `let`. O guard falhava na sua própria branch (confirmei que o arquivo e o `chatCore.ts` eram byte-idênticos ao head da PR, então não era efeito da leva). Passou a aceitar `const|let` — a intenção do guard é o try/catch em volta da chamada, não a palavra-chave. 2. `tests/integration/skills-pipeline.test.ts` foi de 1156 para 1338 linhas e estourou o `testCap` de 1200. Segui o mesmo caminho que você já tinha tomado em `a1d2d20d` para os testes unitários: extraí os três casos do server-owned tool loop para `tests/integration/server-owned-tool-loop-pipeline.test.ts` (259 linhas), com instância própria do harness. O glob `tests/integration/*.test.ts` pega o arquivo novo sem registro adicional. 3/3 verdes isolados. 3. O arquivo novo herdou cinco `any` do original — que só passavam por estarem congelados no `eslint-suppressions.json` sob o nome antigo. Tipei como `Record<string, unknown>`. E `tests/unit/non-streaming-finalization.test.ts` tinha dois argumentos não usados em `trackPendingRequest`, agora prefixados com `_`. Nada disso toca produção nem enfraquece asserção.
176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
import type {
|
|
ExecutionContext,
|
|
NonStreamingProviderLegResult,
|
|
ProviderLegUsage,
|
|
ServerOwnedToolLoopResult,
|
|
ToolCall,
|
|
} from "@/lib/skills/toolLoopTypes.ts";
|
|
import { executeServerOwned } from "@/lib/skills/interception";
|
|
import { runServerOwnedToolLoop, LOOP_BUDGET_MS } from "@/lib/skills/serverOwnedToolLoop.ts";
|
|
import { deriveToolRequestIdentity } from "@/lib/skills/stableJson.ts";
|
|
import { getIdempotencyKey } from "@/lib/idempotencyLayer";
|
|
import { runNonStreamingProviderLeg } from "./nonStreamingProviderLeg.ts";
|
|
import type { ProviderLegInput } from "./nonStreamingProviderLeg.ts";
|
|
import { shouldRunServerOwnedToolLoop } from "./serverOwnedToolLoopGate.ts";
|
|
import { FORMATS } from "../../translator/formats.ts";
|
|
|
|
export function derivePostInjectionRequestIdentity(input: {
|
|
apiKeyId: string;
|
|
headers: unknown;
|
|
skillRequestId: string;
|
|
postInjectionBody: Record<string, unknown>;
|
|
}): string {
|
|
const stableClientRequestId = getIdempotencyKey(input.headers as never);
|
|
return deriveToolRequestIdentity({
|
|
apiKeyId: input.apiKeyId,
|
|
stableClientRequestId,
|
|
skillRequestId: input.skillRequestId,
|
|
postInjectionBody: input.postInjectionBody,
|
|
});
|
|
}
|
|
|
|
export async function continueServerOwnedToolLoop(input: {
|
|
initialLeg: NonStreamingProviderLegResult & { kind: "ok" };
|
|
sourceBody: Record<string, unknown>;
|
|
sourceFormat: "openai" | "claude";
|
|
skillsModelId: string;
|
|
executionContext: ExecutionContext;
|
|
abortSignal?: AbortSignal;
|
|
deadlineAtMs: number;
|
|
expectedConnectionId?: string;
|
|
followUpLeg: (nextSourceBody: Record<string, unknown>) => Promise<NonStreamingProviderLegResult>;
|
|
executeServerOwned?: (
|
|
calls: ToolCall[],
|
|
context: ExecutionContext
|
|
) => Promise<import("@/lib/skills/toolLoopTypes.ts").ExecutedToolResult[]>;
|
|
}): Promise<ServerOwnedToolLoopResult> {
|
|
const runOwned = input.executeServerOwned ?? executeServerOwned;
|
|
return runServerOwnedToolLoop({
|
|
initialLeg: input.initialLeg,
|
|
sourceBody: input.sourceBody,
|
|
sourceFormat: input.sourceFormat,
|
|
skillsModelId: input.skillsModelId,
|
|
executionContext: input.executionContext,
|
|
abortSignal: input.abortSignal,
|
|
deadlineAtMs: input.deadlineAtMs,
|
|
executeServerOwned: (calls: ToolCall[], context: ExecutionContext) => runOwned(calls, context),
|
|
resumeUpstream: async (nextSourceBody, expectedConnectionId) => {
|
|
if (
|
|
expectedConnectionId &&
|
|
input.expectedConnectionId &&
|
|
expectedConnectionId !== input.expectedConnectionId
|
|
) {
|
|
return {
|
|
kind: "error",
|
|
result: {
|
|
success: false,
|
|
status: 409,
|
|
response: new Response(null, { status: 409 }),
|
|
error: "Follow-up connection mismatch",
|
|
errorCode: "LEASE_CONNECTION_MISMATCH",
|
|
},
|
|
receipt: input.initialLeg.receipt,
|
|
usage: null,
|
|
};
|
|
}
|
|
return input.followUpLeg(nextSourceBody);
|
|
},
|
|
});
|
|
}
|
|
|
|
export function followUpLegInput(
|
|
base: Omit<
|
|
ProviderLegInput,
|
|
"phase" | "allowAccountRotation" | "allowModelFallback" | "sourceBody"
|
|
>,
|
|
nextSourceBody: Record<string, unknown>,
|
|
expectedConnectionId?: string
|
|
): ProviderLegInput {
|
|
return {
|
|
...base,
|
|
phase: "follow-up",
|
|
sourceBody: nextSourceBody,
|
|
expectedConnectionId,
|
|
allowAccountRotation: false,
|
|
allowModelFallback: false,
|
|
};
|
|
}
|
|
|
|
export function mergeLoopIntoOkLeg(
|
|
leg: NonStreamingProviderLegResult & { kind: "ok" },
|
|
loop: ServerOwnedToolLoopResult
|
|
): NonStreamingProviderLegResult & { kind: "ok" } {
|
|
return {
|
|
...leg,
|
|
response: loop.response ?? leg.response,
|
|
responseForMemoryExtraction:
|
|
loop.responseForMemoryExtraction ?? leg.responseForMemoryExtraction,
|
|
providerBody: loop.finalProviderBody ?? leg.providerBody,
|
|
providerRequest: loop.finalProviderRequest ?? leg.providerRequest,
|
|
usage: loop.cumulativeUsage,
|
|
};
|
|
}
|
|
|
|
export type ToolLoopApplyResult =
|
|
| { kind: "skip" }
|
|
| {
|
|
kind: "ok";
|
|
leg: NonStreamingProviderLegResult & { kind: "ok" };
|
|
usage: ProviderLegUsage | null;
|
|
loop: ServerOwnedToolLoopResult;
|
|
}
|
|
| { kind: "error"; loop: ServerOwnedToolLoopResult };
|
|
|
|
export async function applyServerOwnedToolLoopIfNeeded(input: {
|
|
enabled: boolean;
|
|
stream: boolean;
|
|
isResponsesEndpoint: boolean;
|
|
sourceFormat: string;
|
|
initialLeg: NonStreamingProviderLegResult;
|
|
sourceBody: Record<string, unknown>;
|
|
skillsModelId: string;
|
|
executionContext: ExecutionContext;
|
|
abortSignal?: AbortSignal;
|
|
expectedConnectionId?: string;
|
|
followUpLeg: (nextSourceBody: Record<string, unknown>) => Promise<NonStreamingProviderLegResult>;
|
|
logReceipt: (receipt: ServerOwnedToolLoopResult["receipts"][number]) => void;
|
|
executeServerOwned?: (
|
|
calls: ToolCall[],
|
|
context: ExecutionContext
|
|
) => Promise<import("@/lib/skills/toolLoopTypes.ts").ExecutedToolResult[]>;
|
|
}): Promise<ToolLoopApplyResult> {
|
|
if (
|
|
input.initialLeg.kind !== "ok" ||
|
|
!shouldRunServerOwnedToolLoop({
|
|
enabled: input.enabled,
|
|
stream: input.stream,
|
|
isResponsesEndpoint: input.isResponsesEndpoint,
|
|
sourceFormat: input.sourceFormat,
|
|
})
|
|
) {
|
|
return { kind: "skip" };
|
|
}
|
|
const loop = await continueServerOwnedToolLoop({
|
|
initialLeg: input.initialLeg,
|
|
sourceBody: input.sourceBody,
|
|
sourceFormat: input.sourceFormat === FORMATS.CLAUDE ? "claude" : "openai",
|
|
skillsModelId: input.skillsModelId,
|
|
executionContext: input.executionContext,
|
|
abortSignal: input.abortSignal,
|
|
deadlineAtMs: Date.now() + LOOP_BUDGET_MS,
|
|
expectedConnectionId: input.expectedConnectionId,
|
|
followUpLeg: input.followUpLeg,
|
|
executeServerOwned: input.executeServerOwned,
|
|
});
|
|
for (const receipt of loop.receipts) input.logReceipt(receipt);
|
|
if (loop.kind === "error") return { kind: "error", loop };
|
|
return {
|
|
kind: "ok",
|
|
leg: mergeLoopIntoOkLeg(input.initialLeg, loop),
|
|
usage: loop.cumulativeUsage,
|
|
loop,
|
|
};
|
|
}
|
|
|
|
export { LOOP_BUDGET_MS, runNonStreamingProviderLeg };
|