@mcoda/codali 0.1.133 → 0.1.135-codali.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/ORCHESTRATION.md +387 -0
- package/README.md +103 -1
- package/config/alternate-local.example.json +49 -0
- package/config/docdex-runtime.json +21 -0
- package/config/orchestration.schema.json +548 -0
- package/config/raijin-suku.example.json +172 -0
- package/config/terminal-long-scenarios.v1.json +70 -0
- package/dist/agents/RoleResolution.d.ts.map +1 -1
- package/dist/agents/RoleResolution.js +4 -4
- package/dist/api/CodaliApi.d.ts +16 -0
- package/dist/api/CodaliApi.d.ts.map +1 -1
- package/dist/api/CodaliApi.js +39 -10
- package/dist/cli/AskCommand.d.ts +3 -0
- package/dist/cli/AskCommand.d.ts.map +1 -1
- package/dist/cli/AskCommand.js +76 -13
- package/dist/cli/RunCommand.d.ts +4 -0
- package/dist/cli/RunCommand.d.ts.map +1 -1
- package/dist/cli/RunCommand.js +110 -24
- package/dist/cli/TerminalCommand.d.ts +35 -0
- package/dist/cli/TerminalCommand.d.ts.map +1 -0
- package/dist/cli/TerminalCommand.js +616 -0
- package/dist/cli/TerminalCredentials.d.ts +3 -0
- package/dist/cli/TerminalCredentials.d.ts.map +1 -0
- package/dist/cli/TerminalCredentials.js +89 -0
- package/dist/cli/TerminalInput.d.ts +24 -0
- package/dist/cli/TerminalInput.d.ts.map +1 -0
- package/dist/cli/TerminalInput.js +98 -0
- package/dist/cli/TerminalText.d.ts +10 -0
- package/dist/cli/TerminalText.d.ts.map +1 -0
- package/dist/cli/TerminalText.js +53 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +33 -4
- package/dist/cognitive/ConfiguredPipeline.d.ts +34 -0
- package/dist/cognitive/ConfiguredPipeline.d.ts.map +1 -0
- package/dist/cognitive/ConfiguredPipeline.js +50 -0
- package/dist/cognitive/ValidationRunner.d.ts +4 -0
- package/dist/cognitive/ValidationRunner.d.ts.map +1 -1
- package/dist/cognitive/ValidationRunner.js +18 -41
- package/dist/config/NodeOrchestration.d.ts +21 -0
- package/dist/config/NodeOrchestration.d.ts.map +1 -0
- package/dist/config/NodeOrchestration.js +54 -0
- package/dist/config/OrchestrationConfig.d.ts +100 -0
- package/dist/config/OrchestrationConfig.d.ts.map +1 -0
- package/dist/config/OrchestrationConfig.js +245 -0
- package/dist/connectors/mcp/McpClient.d.ts +3 -1
- package/dist/connectors/mcp/McpClient.d.ts.map +1 -1
- package/dist/connectors/mcp/McpClient.js +3 -2
- package/dist/connectors/mcp/McpServerRegistry.d.ts +2 -2
- package/dist/connectors/mcp/McpServerRegistry.d.ts.map +1 -1
- package/dist/connectors/mcp/McpServerRegistry.js +39 -16
- package/dist/connectors/mcp/McpToolAdapter.js +2 -2
- package/dist/connectors/media/ConfiguredMediaTools.d.ts +6 -0
- package/dist/connectors/media/ConfiguredMediaTools.d.ts.map +1 -0
- package/dist/connectors/media/ConfiguredMediaTools.js +41 -0
- package/dist/connectors/media/ImageGenerationTool.js +3 -3
- package/dist/connectors/media/MediaArtifactReceipt.d.ts +5 -0
- package/dist/connectors/media/MediaArtifactReceipt.d.ts.map +1 -0
- package/dist/connectors/media/MediaArtifactReceipt.js +65 -0
- package/dist/connectors/media/MediaJobStore.d.ts +68 -0
- package/dist/connectors/media/MediaJobStore.d.ts.map +1 -0
- package/dist/connectors/media/MediaJobStore.js +199 -0
- package/dist/connectors/media/MediaJobWorker.d.ts +2 -0
- package/dist/connectors/media/MediaJobWorker.d.ts.map +1 -0
- package/dist/connectors/media/MediaJobWorker.js +65 -0
- package/dist/connectors/media/RemoteMediaJob.d.ts +6 -0
- package/dist/connectors/media/RemoteMediaJob.d.ts.map +1 -0
- package/dist/connectors/media/RemoteMediaJob.js +80 -0
- package/dist/docdex/DocdexClient.d.ts +18 -1
- package/dist/docdex/DocdexClient.d.ts.map +1 -1
- package/dist/docdex/DocdexClient.js +135 -19
- package/dist/docdex/ManagedDocdex.d.ts +29 -0
- package/dist/docdex/ManagedDocdex.d.ts.map +1 -0
- package/dist/docdex/ManagedDocdex.js +215 -0
- package/dist/docdex/ManagedRuntime.d.ts +28 -0
- package/dist/docdex/ManagedRuntime.d.ts.map +1 -0
- package/dist/docdex/ManagedRuntime.js +212 -0
- package/dist/eval/EvaluationProcess.d.ts +13 -0
- package/dist/eval/EvaluationProcess.d.ts.map +1 -0
- package/dist/eval/EvaluationProcess.js +76 -0
- package/dist/eval/LongWorkflowJourney.d.ts +34 -0
- package/dist/eval/LongWorkflowJourney.d.ts.map +1 -0
- package/dist/eval/LongWorkflowJourney.js +243 -0
- package/dist/eval/PlanningCorpus.d.ts +45 -0
- package/dist/eval/PlanningCorpus.d.ts.map +1 -0
- package/dist/eval/PlanningCorpus.js +189 -0
- package/dist/eval/PlanningCorpusRunner.d.ts +13 -0
- package/dist/eval/PlanningCorpusRunner.d.ts.map +1 -0
- package/dist/eval/PlanningCorpusRunner.js +89 -0
- package/dist/eval/TerminalCorpusRunner.d.ts +36 -0
- package/dist/eval/TerminalCorpusRunner.d.ts.map +1 -0
- package/dist/eval/TerminalCorpusRunner.js +222 -0
- package/dist/eval/TerminalTaskCorpus.d.ts +26 -0
- package/dist/eval/TerminalTaskCorpus.d.ts.map +1 -0
- package/dist/eval/TerminalTaskCorpus.js +105 -0
- package/dist/eval/TerminalWorkflowCorpus.d.ts +11 -0
- package/dist/eval/TerminalWorkflowCorpus.d.ts.map +1 -0
- package/dist/eval/TerminalWorkflowCorpus.js +116 -0
- package/dist/eval/fixtures/TerminalRecoveryJourney.d.ts +4 -0
- package/dist/eval/fixtures/TerminalRecoveryJourney.d.ts.map +1 -0
- package/dist/eval/fixtures/TerminalRecoveryJourney.js +132 -0
- package/dist/gateway/CodaliGateway.d.ts +2 -0
- package/dist/gateway/CodaliGateway.d.ts.map +1 -1
- package/dist/gateway/CodaliGateway.js +16 -4
- package/dist/gateway/ConfiguredGateway.d.ts +23 -0
- package/dist/gateway/ConfiguredGateway.d.ts.map +1 -0
- package/dist/gateway/ConfiguredGateway.js +44 -0
- package/dist/gateway/GatewayTracer.d.ts +1 -1
- package/dist/gateway/GatewayTracer.d.ts.map +1 -1
- package/dist/gateway/GatewayTracer.js +2 -27
- package/dist/gateway/LocalGatewayTaskRunner.d.ts.map +1 -1
- package/dist/gateway/LocalGatewayTaskRunner.js +13 -1
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/permissions/PermissionGate.d.ts +44 -0
- package/dist/permissions/PermissionGate.d.ts.map +1 -0
- package/dist/permissions/PermissionGate.js +97 -0
- package/dist/providers/ConfiguredEmbeddings.d.ts +43 -0
- package/dist/providers/ConfiguredEmbeddings.d.ts.map +1 -0
- package/dist/providers/ConfiguredEmbeddings.js +87 -0
- package/dist/providers/ConfiguredModelScope.d.ts +69 -0
- package/dist/providers/ConfiguredModelScope.d.ts.map +1 -0
- package/dist/providers/ConfiguredModelScope.js +113 -0
- package/dist/providers/ConfiguredProvider.d.ts +79 -0
- package/dist/providers/ConfiguredProvider.d.ts.map +1 -0
- package/dist/providers/ConfiguredProvider.js +238 -0
- package/dist/providers/ModelServiceError.d.ts +10 -0
- package/dist/providers/ModelServiceError.d.ts.map +1 -0
- package/dist/providers/ModelServiceError.js +45 -0
- package/dist/providers/MswarmWorkerProvider.d.ts.map +1 -1
- package/dist/providers/MswarmWorkerProvider.js +4 -0
- package/dist/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
- package/dist/providers/OpenAiCompatibleProvider.js +134 -52
- package/dist/providers/ProviderTypes.d.ts +14 -0
- package/dist/providers/ProviderTypes.d.ts.map +1 -1
- package/dist/runtime/CodaliJobRuntime.d.ts +6 -0
- package/dist/runtime/CodaliJobRuntime.d.ts.map +1 -1
- package/dist/runtime/CodaliJobRuntime.js +36 -1
- package/dist/runtime/CodaliRuntime.d.ts +30 -0
- package/dist/runtime/CodaliRuntime.d.ts.map +1 -1
- package/dist/runtime/CodaliRuntime.js +71 -1
- package/dist/runtime/CompletionReport.d.ts +5 -0
- package/dist/runtime/CompletionReport.d.ts.map +1 -0
- package/dist/runtime/CompletionReport.js +12 -0
- package/dist/runtime/ProgressTracker.d.ts +17 -0
- package/dist/runtime/ProgressTracker.d.ts.map +1 -0
- package/dist/runtime/ProgressTracker.js +27 -0
- package/dist/runtime/RequestedTestEvidence.d.ts +12 -0
- package/dist/runtime/RequestedTestEvidence.d.ts.map +1 -0
- package/dist/runtime/RequestedTestEvidence.js +16 -0
- package/dist/runtime/RequiredRetrieval.d.ts +13 -0
- package/dist/runtime/RequiredRetrieval.d.ts.map +1 -0
- package/dist/runtime/RequiredRetrieval.js +25 -0
- package/dist/runtime/ReviewSource.d.ts +6 -0
- package/dist/runtime/ReviewSource.d.ts.map +1 -0
- package/dist/runtime/ReviewSource.js +38 -0
- package/dist/runtime/Runner.d.ts +13 -0
- package/dist/runtime/Runner.d.ts.map +1 -1
- package/dist/runtime/Runner.js +220 -158
- package/dist/runtime/SecretRedaction.d.ts +11 -0
- package/dist/runtime/SecretRedaction.d.ts.map +1 -0
- package/dist/runtime/SecretRedaction.js +85 -0
- package/dist/runtime/SessionController.d.ts +148 -0
- package/dist/runtime/SessionController.d.ts.map +1 -0
- package/dist/runtime/SessionController.js +1356 -0
- package/dist/runtime/StructuralEvidence.d.ts +22 -0
- package/dist/runtime/StructuralEvidence.d.ts.map +1 -0
- package/dist/runtime/StructuralEvidence.js +87 -0
- package/dist/runtime/TaskReviewContext.d.ts +34 -0
- package/dist/runtime/TaskReviewContext.d.ts.map +1 -0
- package/dist/runtime/TaskReviewContext.js +34 -0
- package/dist/runtime/ValidationOutput.d.ts +5 -0
- package/dist/runtime/ValidationOutput.d.ts.map +1 -0
- package/dist/runtime/ValidationOutput.js +32 -0
- package/dist/runtime/VerificationCommand.d.ts +3 -0
- package/dist/runtime/VerificationCommand.d.ts.map +1 -0
- package/dist/runtime/VerificationCommand.js +48 -0
- package/dist/runtime/WorkStatusPage.d.ts +15 -0
- package/dist/runtime/WorkStatusPage.d.ts.map +1 -0
- package/dist/runtime/WorkStatusPage.js +58 -0
- package/dist/runtime/WorkspaceLedger.d.ts +13 -0
- package/dist/runtime/WorkspaceLedger.d.ts.map +1 -0
- package/dist/runtime/WorkspaceLedger.js +145 -0
- package/dist/runtime/WorkspaceLock.d.ts.map +1 -1
- package/dist/runtime/WorkspaceLock.js +20 -17
- package/dist/runtime/WorkspaceSnapshot.d.ts +13 -0
- package/dist/runtime/WorkspaceSnapshot.d.ts.map +1 -0
- package/dist/runtime/WorkspaceSnapshot.js +88 -0
- package/dist/session/AtomicState.d.ts +13 -0
- package/dist/session/AtomicState.d.ts.map +1 -0
- package/dist/session/AtomicState.js +109 -0
- package/dist/session/SessionStoragePath.d.ts +15 -0
- package/dist/session/SessionStoragePath.d.ts.map +1 -0
- package/dist/session/SessionStoragePath.js +89 -0
- package/dist/session/SessionStore.d.ts +9 -0
- package/dist/session/SessionStore.d.ts.map +1 -1
- package/dist/session/SessionStore.js +181 -47
- package/dist/session/WorkState.d.ts +23 -0
- package/dist/session/WorkState.d.ts.map +1 -0
- package/dist/session/WorkState.js +367 -0
- package/dist/session/WorkTypes.d.ts +94 -0
- package/dist/session/WorkTypes.d.ts.map +1 -0
- package/dist/session/WorkTypes.js +1 -0
- package/dist/tools/ToolRegistry.d.ts +2 -0
- package/dist/tools/ToolRegistry.d.ts.map +1 -1
- package/dist/tools/ToolRegistry.js +12 -2
- package/dist/tools/ToolTypes.d.ts +16 -0
- package/dist/tools/ToolTypes.d.ts.map +1 -1
- package/dist/tools/docdex/DocdexTools.d.ts +2 -0
- package/dist/tools/docdex/DocdexTools.d.ts.map +1 -1
- package/dist/tools/docdex/DocdexTools.js +17 -2
- package/dist/tools/filesystem/FileTools.d.ts.map +1 -1
- package/dist/tools/filesystem/FileTools.js +30 -10
- package/dist/tools/shell/AsyncProcess.d.ts +16 -0
- package/dist/tools/shell/AsyncProcess.d.ts.map +1 -0
- package/dist/tools/shell/AsyncProcess.js +94 -0
- package/dist/tools/shell/DarwinRuntimeLibraries.d.ts +13 -0
- package/dist/tools/shell/DarwinRuntimeLibraries.d.ts.map +1 -0
- package/dist/tools/shell/DarwinRuntimeLibraries.js +130 -0
- package/dist/tools/shell/HostToolchain.d.ts +19 -0
- package/dist/tools/shell/HostToolchain.d.ts.map +1 -0
- package/dist/tools/shell/HostToolchain.js +264 -0
- package/dist/tools/shell/IsolatedExecutor.d.ts +15 -0
- package/dist/tools/shell/IsolatedExecutor.d.ts.map +1 -0
- package/dist/tools/shell/IsolatedExecutor.js +134 -0
- package/dist/tools/shell/ShellTool.d.ts.map +1 -1
- package/dist/tools/shell/ShellTool.js +9 -29
- package/dist/tools/shell/ShellWorkingDirectory.d.ts +3 -0
- package/dist/tools/shell/ShellWorkingDirectory.d.ts.map +1 -0
- package/dist/tools/shell/ShellWorkingDirectory.js +30 -0
- package/package.json +27 -24
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { CryptoHelper, PathHelper, mswarmClientProductHeaders } from "@mcoda/shared";
|
|
4
|
+
import { resolveCredential } from "../runcontext/CredentialFile.js";
|
|
5
|
+
import { resolveBinding } from "../config/OrchestrationConfig.js";
|
|
6
|
+
import { OpenAiCompatibleProvider } from "./OpenAiCompatibleProvider.js";
|
|
7
|
+
import { ModelServiceError } from "./ModelServiceError.js";
|
|
8
|
+
import { registerSecretValue } from "../runtime/SecretRedaction.js";
|
|
9
|
+
export const normalizeModelCaller = (caller = {}) => {
|
|
10
|
+
const clientIdentity = caller.clientIdentity?.trim();
|
|
11
|
+
const clientProduct = caller.clientProduct?.trim().toLowerCase() ?? "codali";
|
|
12
|
+
if (clientIdentity !== undefined && !/^[A-Za-z0-9._:-]{1,128}$/.test(clientIdentity))
|
|
13
|
+
throw new Error("Invalid model client identity");
|
|
14
|
+
if (!/^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$/.test(clientProduct))
|
|
15
|
+
throw new Error("Invalid model client product");
|
|
16
|
+
return { ...(clientIdentity ? { clientIdentity } : {}), clientProduct };
|
|
17
|
+
};
|
|
18
|
+
const modelCallerHeaders = (caller) => ({
|
|
19
|
+
...mswarmClientProductHeaders(caller.clientProduct),
|
|
20
|
+
...(caller.clientIdentity ? { "x-mswarm-client-identity": caller.clientIdentity, "x-mswarm-client": caller.clientIdentity } : {}),
|
|
21
|
+
});
|
|
22
|
+
const record = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
23
|
+
/** Discovery failed before inference; a caller may release its local token reservation. */
|
|
24
|
+
export class NativeReasoningTransportError extends Error {
|
|
25
|
+
constructor(message) { super(message); this.name = "NativeReasoningTransportError"; }
|
|
26
|
+
}
|
|
27
|
+
export const providerCredential = async (provider) => {
|
|
28
|
+
if (!provider.credentialRef)
|
|
29
|
+
return undefined;
|
|
30
|
+
const name = provider.credentialRef.slice(4);
|
|
31
|
+
const direct = resolveCredential(name);
|
|
32
|
+
if (direct) {
|
|
33
|
+
registerSecretValue(direct);
|
|
34
|
+
return direct;
|
|
35
|
+
}
|
|
36
|
+
if (provider.adapter === "mswarm" && resolveCredential("CODALI_MSWARM_LOGGED_OUT") === "1")
|
|
37
|
+
throw new Error("Logged out; use codali login");
|
|
38
|
+
// Migrate by reading the existing user store; never read repository credentials.
|
|
39
|
+
if (provider.adapter === "mswarm" && name === "MCODA_MSWARM_API_KEY") {
|
|
40
|
+
try {
|
|
41
|
+
const config = JSON.parse(await readFile(PathHelper.getGlobalConfigPath(), "utf8"));
|
|
42
|
+
const existing = config.mswarm;
|
|
43
|
+
if (existing?.encryptedApiKey && new URL(existing.baseUrl ?? "https://api.mswarm.org").origin === new URL(provider.baseUrl).origin) {
|
|
44
|
+
const key = await CryptoHelper.decryptSecret(existing.encryptedApiKey);
|
|
45
|
+
registerSecretValue(key);
|
|
46
|
+
return key;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== "ENOENT")
|
|
51
|
+
throw new Error("Stored mSwarm credentials could not be loaded; use codali login");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Missing credential ${name}; use codali login or set the environment variable`);
|
|
55
|
+
};
|
|
56
|
+
export class ModelServiceClient {
|
|
57
|
+
constructor(provider, credentialOverride, caller) {
|
|
58
|
+
this.provider = provider;
|
|
59
|
+
this.credentialOverride = credentialOverride;
|
|
60
|
+
this.caller = normalizeModelCaller(caller);
|
|
61
|
+
if (credentialOverride)
|
|
62
|
+
registerSecretValue(credentialOverride);
|
|
63
|
+
}
|
|
64
|
+
credential() { return this.credentialOverride === undefined ? providerCredential(this.provider) : Promise.resolve(this.credentialOverride); }
|
|
65
|
+
async json(url, options = {}) {
|
|
66
|
+
if (new URL(url).origin !== new URL(this.provider.baseUrl).origin)
|
|
67
|
+
throw new Error("Refusing credentials to an unconfigured origin");
|
|
68
|
+
const key = await this.credential();
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
71
|
+
options.signal?.throwIfAborted();
|
|
72
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
73
|
+
const timer = setTimeout(() => controller.abort(new Error("Service request timed out")), options.timeoutMs ?? 30000);
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
redirect: "error", method: options.method ?? "GET", signal: controller.signal,
|
|
77
|
+
headers: { accept: "application/json", ...(key ? this.provider.adapter === "mswarm" ? { "x-api-key": key } : { authorization: `Bearer ${key}` } : {}), ...(this.provider.adapter === "mswarm" ? modelCallerHeaders(this.caller) : {}), ...(options.body ? { "content-type": "application/json" } : {}) },
|
|
78
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw await ModelServiceError.fromResponse(response);
|
|
82
|
+
if (!response.body)
|
|
83
|
+
throw new Error("Empty service response");
|
|
84
|
+
const reader = response.body.getReader();
|
|
85
|
+
const chunks = [];
|
|
86
|
+
let size = 0;
|
|
87
|
+
try {
|
|
88
|
+
while (true) {
|
|
89
|
+
const next = await reader.read();
|
|
90
|
+
if (next.done)
|
|
91
|
+
break;
|
|
92
|
+
size += next.value.byteLength;
|
|
93
|
+
if (size > 96 * 1024 * 1024) {
|
|
94
|
+
await reader.cancel();
|
|
95
|
+
throw new Error("Service response exceeds size limit");
|
|
96
|
+
}
|
|
97
|
+
chunks.push(next.value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
reader.releaseLock();
|
|
102
|
+
}
|
|
103
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
options.signal?.removeEventListener("abort", abort);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async catalog(signal, refresh = false) {
|
|
111
|
+
const credential = await this.credential();
|
|
112
|
+
const key = createHash("sha256").update(credential ?? "anonymous").digest("hex");
|
|
113
|
+
if (!refresh && this.cache?.key === key && this.cache.expires > Date.now())
|
|
114
|
+
return this.cache.entries;
|
|
115
|
+
const base = new URL(this.provider.baseUrl);
|
|
116
|
+
const url = this.provider.catalogUrl ?? (this.provider.adapter === "mswarm" ? new URL("/v1/swarm/self-hosted/agents", base).toString() : `${this.provider.baseUrl.replace(/\/$/, "")}/models`);
|
|
117
|
+
const payload = record(await this.json(url, { signal }));
|
|
118
|
+
const items = payload.agents ?? payload.data;
|
|
119
|
+
if (!Array.isArray(items))
|
|
120
|
+
throw new Error("Invalid model catalog response");
|
|
121
|
+
const entries = items.map(item => {
|
|
122
|
+
const raw = record(item);
|
|
123
|
+
const aliases = [raw.slug, raw.public_model_id, raw.default_model, raw.id].filter((v) => typeof v === "string" && !!v);
|
|
124
|
+
return { id: aliases[0] ?? "", aliases, backend: raw.adapter === "strata-local" ? "strata" : typeof raw.backend === "string" ? raw.backend : typeof raw.adapter === "string" ? raw.adapter : undefined,
|
|
125
|
+
health: String(raw.health_status ?? "unknown"), supportsTools: raw.supports_tools === true,
|
|
126
|
+
contextWindow: typeof raw.context_window === "number" ? raw.context_window : undefined,
|
|
127
|
+
operations: Array.isArray(raw.operations) ? raw.operations.map(v => typeof v === "string" ? v : String(record(v).type ?? "")) : [], raw };
|
|
128
|
+
}).filter(x => x.id);
|
|
129
|
+
this.cache = { key, expires: Date.now() + 30000, entries };
|
|
130
|
+
return entries;
|
|
131
|
+
}
|
|
132
|
+
async usage(signal) {
|
|
133
|
+
if (this.provider.adapter !== "mswarm" && !this.provider.usageUrl)
|
|
134
|
+
return { available: false };
|
|
135
|
+
return this.json(this.provider.usageUrl ?? new URL("/v1/swarm/runtime/usage-limits", this.provider.baseUrl).toString(), { signal });
|
|
136
|
+
}
|
|
137
|
+
/** The proxy and selected node must both advertise this transport contract.
|
|
138
|
+
* A model's supports_reasoning flag alone says nothing about request forwarding. */
|
|
139
|
+
async requireNativeReasoning(modelId, parameters, signal) {
|
|
140
|
+
if (this.provider.adapter !== "mswarm" || parameters.length === 0)
|
|
141
|
+
return;
|
|
142
|
+
const endpoint = new URL(this.provider.baseUrl);
|
|
143
|
+
endpoint.pathname = endpoint.pathname.replace(/\/$/, "");
|
|
144
|
+
endpoint.pathname = ["", "/v1"].includes(endpoint.pathname)
|
|
145
|
+
? "/v1/swarm/self-hosted/openai/models" : `${endpoint.pathname}/models`;
|
|
146
|
+
const payload = record(await this.json(endpoint.href, { signal }).catch(error => {
|
|
147
|
+
throw new NativeReasoningTransportError(`Native reasoning preflight failed before inference: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
}));
|
|
149
|
+
const models = Array.isArray(payload.data) ? payload.data.map(record) : [];
|
|
150
|
+
const model = models.find(item => [item.id, item.agent_slug, item.remote_slug].includes(modelId));
|
|
151
|
+
const contract = record(model?.native_reasoning);
|
|
152
|
+
const supported = Array.isArray(contract.parameters) ? contract.parameters : [];
|
|
153
|
+
if (contract.transport_version !== 1 || parameters.some(parameter => !supported.includes(parameter))) {
|
|
154
|
+
throw new NativeReasoningTransportError(`Native reasoning transport is unavailable for ${modelId}; upgrade the mSwarm proxy and selected node, or remove unsupported native reasoning settings from the configuration`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export class ConfiguredProvider {
|
|
159
|
+
constructor(resolved, binding = "lead", options = {}) {
|
|
160
|
+
this.resolved = resolved;
|
|
161
|
+
this.binding = binding;
|
|
162
|
+
this.options = options;
|
|
163
|
+
this.name = "configured";
|
|
164
|
+
this.supportsToolCalls = true;
|
|
165
|
+
this.clients = new Map();
|
|
166
|
+
this.caller = normalizeModelCaller(options.caller);
|
|
167
|
+
this.credentials = Object.freeze(Object.assign(Object.create(null), options.credentials));
|
|
168
|
+
for (const [id, credential] of Object.entries(this.credentials)) {
|
|
169
|
+
if (!Object.prototype.hasOwnProperty.call(resolved.config.providers, id) || typeof credential !== "string" || !credential.trim())
|
|
170
|
+
throw new Error("Invalid host provider credential mapping");
|
|
171
|
+
registerSecretValue(credential);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
client(providerId) {
|
|
175
|
+
const provider = this.resolved.config.providers[providerId];
|
|
176
|
+
if (!provider)
|
|
177
|
+
throw new Error(`Unknown configured provider: ${providerId}`);
|
|
178
|
+
if (!this.clients.has(providerId))
|
|
179
|
+
this.clients.set(providerId, new ModelServiceClient(provider, this.credentials[providerId], this.caller));
|
|
180
|
+
return this.clients.get(providerId);
|
|
181
|
+
}
|
|
182
|
+
async select(operation, signal, requireTools = false) {
|
|
183
|
+
const candidates = resolveBinding(this.resolved.config, this.binding, operation);
|
|
184
|
+
const reasons = [];
|
|
185
|
+
for (const candidate of candidates) {
|
|
186
|
+
const { id, model } = candidate;
|
|
187
|
+
const catalog = await this.client(model.provider).catalog(signal);
|
|
188
|
+
const actual = catalog.find(entry => entry.aliases.includes(model.catalogId));
|
|
189
|
+
if (!actual) {
|
|
190
|
+
reasons.push(`${id}: not entitled/advertised`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!["healthy", "unknown", "ready"].includes(actual.health)) {
|
|
194
|
+
reasons.push(`${id}: ${actual.health}`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (model.backend && actual.backend !== model.backend) {
|
|
198
|
+
reasons.push(`${id}: backend identity not verified`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (actual.operations.length && !actual.operations.includes(operation)) {
|
|
202
|
+
reasons.push(`${id}: unsupported operation`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (requireTools && (!model.supportsTools || this.resolved.config.providers[model.provider].adapter === "mswarm" && !actual.supportsTools)) {
|
|
206
|
+
reasons.push(`${id}: tools unavailable`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
return { id, model, actual, provider: this.resolved.config.providers[model.provider] };
|
|
210
|
+
}
|
|
211
|
+
throw new Error(`No eligible configured ${this.binding} route: ${reasons.join("; ")}`);
|
|
212
|
+
}
|
|
213
|
+
async generate(request) {
|
|
214
|
+
request.signal?.throwIfAborted();
|
|
215
|
+
const { id, model, provider, actual } = await this.select("chat.completions", request.signal, !!request.tools?.length);
|
|
216
|
+
const invocation = { id: randomUUID(), binding: this.binding, configHash: this.resolved.hash, modelId: id, catalogId: model.catalogId, backend: actual.backend, host: model.host };
|
|
217
|
+
const nativeReasoning = this.options.effort ? model.reasoning?.[this.options.effort] : undefined;
|
|
218
|
+
if (nativeReasoning)
|
|
219
|
+
invocation.nativeReasoningRequest = { ...nativeReasoning };
|
|
220
|
+
if (nativeReasoning)
|
|
221
|
+
await this.client(model.provider).requireNativeReasoning(model.catalogId, Object.keys(nativeReasoning), request.signal);
|
|
222
|
+
const maxTokens = Math.min(request.maxTokens ?? model.maxOutputTokens, model.maxOutputTokens);
|
|
223
|
+
const inputEstimate = Math.ceil(JSON.stringify([request.messages, request.tools]).length / 3);
|
|
224
|
+
if (inputEstimate + maxTokens > Math.min(model.contextWindow, actual.contextWindow ?? model.contextWindow))
|
|
225
|
+
throw new Error("Context budget exceeded; checkpoint and compact before continuing");
|
|
226
|
+
const bounded = { ...request, maxTokens, stream: request.stream === true && model.supportsStreaming === true };
|
|
227
|
+
await this.options.beforeRequest?.(invocation, bounded);
|
|
228
|
+
const adapter = new OpenAiCompatibleProvider({
|
|
229
|
+
model: model.catalogId, baseUrl: provider.baseUrl, apiKey: this.credentials[model.provider] ?? await providerCredential(provider), timeoutMs: 300000,
|
|
230
|
+
supportsStreaming: model.supportsStreaming ?? true, supportsTools: model.supportsTools,
|
|
231
|
+
headers: provider.adapter === "mswarm" ? modelCallerHeaders(this.caller) : undefined, retryTransient: false,
|
|
232
|
+
extraBody: nativeReasoning,
|
|
233
|
+
});
|
|
234
|
+
const result = await adapter.generate(bounded);
|
|
235
|
+
await this.options.afterRequest?.(invocation, result);
|
|
236
|
+
return { ...result, route: invocation };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ModelServiceErrorCode = "authentication_failed" | "entitlement_missing" | "quota_exhausted" | "rate_limited" | "service_unavailable" | "model_unavailable" | "invalid_request";
|
|
2
|
+
/** Transport diagnostics contain stable classifications, never an upstream body. */
|
|
3
|
+
export declare class ModelServiceError extends Error {
|
|
4
|
+
readonly code: ModelServiceErrorCode;
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
7
|
+
constructor(code: ModelServiceErrorCode, status: number, retryAfterSeconds?: number | undefined);
|
|
8
|
+
static fromResponse(response: Response): Promise<ModelServiceError>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=ModelServiceError.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelServiceError.d.ts","sourceRoot":"","sources":["../../src/providers/ModelServiceError.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG,uBAAuB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,cAAc,GAAG,qBAAqB,GAAG,mBAAmB,GAAG,iBAAiB,CAAC;AAE3L,oFAAoF;AACpF,qBAAa,iBAAkB,SAAQ,KAAK;IAC9B,QAAQ,CAAC,IAAI,EAAE,qBAAqB;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM;IAAE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM;gBAAzF,IAAI,EAAE,qBAAqB,EAAW,MAAM,EAAE,MAAM,EAAW,iBAAiB,CAAC,EAAE,MAAM,YAAA;WAKjG,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAoB1E"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Transport diagnostics contain stable classifications, never an upstream body. */
|
|
2
|
+
export class ModelServiceError extends Error {
|
|
3
|
+
constructor(code, status, retryAfterSeconds) {
|
|
4
|
+
super(`Model service HTTP ${status}: ${code}${code === "authentication_failed" ? "; authenticate with codali login" : ""}`);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
8
|
+
this.name = "ModelServiceError";
|
|
9
|
+
}
|
|
10
|
+
static async fromResponse(response) {
|
|
11
|
+
let serverCode = "";
|
|
12
|
+
if (response.body) {
|
|
13
|
+
const reader = response.body.getReader();
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
try {
|
|
17
|
+
while (true) {
|
|
18
|
+
const part = await reader.read();
|
|
19
|
+
if (part.done)
|
|
20
|
+
break;
|
|
21
|
+
bytes += part.value.byteLength;
|
|
22
|
+
if (bytes > 8192)
|
|
23
|
+
break;
|
|
24
|
+
chunks.push(part.value);
|
|
25
|
+
}
|
|
26
|
+
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
27
|
+
if (typeof (payload.error?.code ?? payload.code) === "string")
|
|
28
|
+
serverCode = String(payload.error?.code ?? payload.code);
|
|
29
|
+
}
|
|
30
|
+
catch { /* The status still supplies a safe classification. */ }
|
|
31
|
+
finally {
|
|
32
|
+
await reader.cancel().catch(() => undefined);
|
|
33
|
+
reader.releaseLock();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const code = response.status === 401 ? "authentication_failed"
|
|
37
|
+
: response.status === 403 ? "entitlement_missing"
|
|
38
|
+
: ["quota_exceeded", "quota_exhausted", "insufficient_quota", "credits_exhausted", "insufficient_credits"].includes(serverCode) || response.status === 402 ? "quota_exhausted"
|
|
39
|
+
: response.status === 429 ? "rate_limited"
|
|
40
|
+
: response.status === 404 ? "model_unavailable"
|
|
41
|
+
: response.status >= 500 ? "service_unavailable" : "invalid_request";
|
|
42
|
+
const retry = Number(response.headers.get("retry-after"));
|
|
43
|
+
return new ModelServiceError(code, response.status, response.headers.has("retry-after") && Number.isFinite(retry) && retry >= 0 ? Math.min(retry, 3600) : undefined);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MswarmWorkerProvider.d.ts","sourceRoot":"","sources":["../../src/providers/MswarmWorkerProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EAER,eAAe,EACf,gBAAgB,EAEjB,MAAM,oBAAoB,CAAC;AA8C5B,qBAAa,oBAAqB,YAAW,QAAQ;IAQvC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAPnC,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,SAAS;IAEnC,QAAQ,CAAC,IAAI,mBAAmB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEH,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAWvG,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"MswarmWorkerProvider.d.ts","sourceRoot":"","sources":["../../src/providers/MswarmWorkerProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EAER,eAAe,EACf,gBAAgB,EAEjB,MAAM,oBAAoB,CAAC;AA8C5B,qBAAa,oBAAqB,YAAW,QAAQ;IAQvC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAPnC,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,SAAS;IAEnC,QAAQ,CAAC,IAAI,mBAAmB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEH,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAWvG,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAmEpE"}
|
|
@@ -52,7 +52,10 @@ export class MswarmWorkerProvider {
|
|
|
52
52
|
this.apiKey = config.apiKey;
|
|
53
53
|
}
|
|
54
54
|
async generate(request) {
|
|
55
|
+
request.signal?.throwIfAborted();
|
|
55
56
|
const controller = new AbortController();
|
|
57
|
+
const onAbort = () => controller.abort(request.signal?.reason);
|
|
58
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
56
59
|
const timeout = this.config.timeoutMs && this.config.timeoutMs > 0
|
|
57
60
|
? setTimeout(() => controller.abort(), this.config.timeoutMs)
|
|
58
61
|
: undefined;
|
|
@@ -106,6 +109,7 @@ export class MswarmWorkerProvider {
|
|
|
106
109
|
};
|
|
107
110
|
}
|
|
108
111
|
finally {
|
|
112
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
109
113
|
if (timeout) {
|
|
110
114
|
clearTimeout(timeout);
|
|
111
115
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpenAiCompatibleProvider.d.ts","sourceRoot":"","sources":["../../src/providers/OpenAiCompatibleProvider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"OpenAiCompatibleProvider.d.ts","sourceRoot":"","sources":["../../src/providers/OpenAiCompatibleProvider.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EAEd,eAAe,EACf,gBAAgB,EAGjB,MAAM,oBAAoB,CAAC;AAkK5B,eAAO,MAAM,uBAAuB,GAClC,KAAK,MAAM,EACX,MAAM,WAAW,EACjB,aAAa,eAAe,KAC3B,OAAO,CAAC,QAAQ,CAclB,CAAC;AAEF,qBAAa,wBAAyB,YAAW,QAAQ;IAS3C,OAAO,CAAC,MAAM;IAR1B,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,QAAQ;IAElC,IAAI,SAAuB;IAC3B,OAAO,CAAC,WAAW,CAAoC;IACvD,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,sBAAsB,CAAoC;gBAE9C,MAAM,EAAE,cAAc;IAWpC,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoPnE,OAAO,CAAC,WAAW;CA0BpB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeLocalOpenAiCompatibleRunnerConfig, } from "@mcoda/shared";
|
|
2
|
+
import { ModelServiceError } from "./ModelServiceError.js";
|
|
2
3
|
const parseToolArgs = (raw) => {
|
|
3
4
|
try {
|
|
4
5
|
return JSON.parse(raw);
|
|
@@ -7,6 +8,11 @@ const parseToolArgs = (raw) => {
|
|
|
7
8
|
return raw;
|
|
8
9
|
}
|
|
9
10
|
};
|
|
11
|
+
// JSON permits lone UTF-16 surrogates, but native tokenizers often require
|
|
12
|
+
// Unicode scalar values. Keep their visible source spelling in model context
|
|
13
|
+
// instead of losing them to replacement characters. With /u, valid pairs are
|
|
14
|
+
// matched as one astral code point and are therefore outside this range.
|
|
15
|
+
const modelContextText = (value) => value.replace(/[\uD800-\uDFFF]/gu, (unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
10
16
|
const normalizeBaseUrl = (baseUrl) => {
|
|
11
17
|
const root = baseUrl ?? "https://api.openai.com/v1";
|
|
12
18
|
return root.endsWith("/") ? root : `${root}/`;
|
|
@@ -45,7 +51,16 @@ const toResponseFormat = (format, strategy = "openai") => {
|
|
|
45
51
|
return { type: "json_object" };
|
|
46
52
|
}
|
|
47
53
|
if (format.type === "json_schema") {
|
|
48
|
-
|
|
54
|
+
const schema = format.schema ?? {};
|
|
55
|
+
// ProviderResponseFormat carries a JSON Schema; OpenAI-compatible servers
|
|
56
|
+
// require it inside a named json_schema envelope. Preserve callers that
|
|
57
|
+
// already supply that envelope.
|
|
58
|
+
const wrapped = typeof schema.name === "string" &&
|
|
59
|
+
schema.schema !== null && typeof schema.schema === "object";
|
|
60
|
+
return {
|
|
61
|
+
type: "json_schema",
|
|
62
|
+
json_schema: wrapped ? schema : { name: "codali_response", schema },
|
|
63
|
+
};
|
|
49
64
|
}
|
|
50
65
|
if (format.type === "text") {
|
|
51
66
|
return { type: "text" };
|
|
@@ -139,6 +154,7 @@ export class OpenAiCompatibleProvider {
|
|
|
139
154
|
this.responseFormatStrategy = normalizedLocal.config.responseFormatStrategy ?? "openai";
|
|
140
155
|
}
|
|
141
156
|
async generate(request) {
|
|
157
|
+
request.signal?.throwIfAborted();
|
|
142
158
|
const baseUrl = normalizeBaseUrl(this.localRunner.baseUrl ?? this.config.baseUrl);
|
|
143
159
|
const url = new URL("chat/completions", baseUrl).toString();
|
|
144
160
|
const auth = this.resolveAuth();
|
|
@@ -159,12 +175,29 @@ export class OpenAiCompatibleProvider {
|
|
|
159
175
|
: undefined;
|
|
160
176
|
const messages = request.messages.map((message) => ({
|
|
161
177
|
role: message.role,
|
|
162
|
-
content: message.content,
|
|
178
|
+
content: modelContextText(message.content),
|
|
163
179
|
name: message.name,
|
|
164
180
|
tool_call_id: message.toolCallId,
|
|
181
|
+
tool_calls: message.toolCalls?.map((call) => ({
|
|
182
|
+
id: call.id,
|
|
183
|
+
type: "function",
|
|
184
|
+
function: {
|
|
185
|
+
name: call.name,
|
|
186
|
+
arguments: typeof call.args === "string" ? modelContextText(call.args) : JSON.stringify(call.args ?? {}),
|
|
187
|
+
},
|
|
188
|
+
})),
|
|
165
189
|
}));
|
|
166
190
|
if (promptOnlyInstruction) {
|
|
167
|
-
|
|
191
|
+
// Local chat templates may accept only one initial system message.
|
|
192
|
+
if (messages[0]?.role === "system") {
|
|
193
|
+
messages[0] = {
|
|
194
|
+
...messages[0],
|
|
195
|
+
content: `${messages[0].content}\n\n${promptOnlyInstruction}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
messages.unshift({ role: "system", content: promptOnlyInstruction });
|
|
200
|
+
}
|
|
168
201
|
}
|
|
169
202
|
const body = {
|
|
170
203
|
...(this.localRunner.requireModelInRequest === false ? {} : { model: this.config.model }),
|
|
@@ -173,7 +206,7 @@ export class OpenAiCompatibleProvider {
|
|
|
173
206
|
type: "function",
|
|
174
207
|
function: {
|
|
175
208
|
name: tool.name,
|
|
176
|
-
description: tool.description,
|
|
209
|
+
description: tool.description === undefined ? undefined : modelContextText(tool.description),
|
|
177
210
|
parameters: tool.inputSchema ?? {},
|
|
178
211
|
},
|
|
179
212
|
})),
|
|
@@ -202,20 +235,23 @@ export class OpenAiCompatibleProvider {
|
|
|
202
235
|
}
|
|
203
236
|
}
|
|
204
237
|
const controller = new AbortController();
|
|
238
|
+
const onAbort = () => controller.abort(request.signal?.reason);
|
|
239
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
205
240
|
const timeoutMs = this.config.timeoutMs ?? 60000;
|
|
206
241
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
207
242
|
try {
|
|
208
|
-
const
|
|
243
|
+
const requestFetch = this.config.retryTransient === false ? (url, init) => fetch(url, init) : fetchWithTransientRetry;
|
|
244
|
+
const response = await requestFetch(url, {
|
|
209
245
|
method: "POST",
|
|
210
246
|
headers,
|
|
211
247
|
body: JSON.stringify(body),
|
|
212
248
|
signal: controller.signal,
|
|
249
|
+
redirect: "error",
|
|
213
250
|
}, controller);
|
|
214
251
|
if (!response.ok) {
|
|
215
|
-
|
|
216
|
-
throw new Error(`OpenAI-compatible error ${response.status}: ${errorBody}`);
|
|
252
|
+
throw await ModelServiceError.fromResponse(response);
|
|
217
253
|
}
|
|
218
|
-
if (request.stream) {
|
|
254
|
+
if (request.stream && !response.headers.get("content-type")?.includes("application/json")) {
|
|
219
255
|
if (!response.body) {
|
|
220
256
|
throw new Error("OpenAI-compatible streaming response missing body");
|
|
221
257
|
}
|
|
@@ -223,55 +259,85 @@ export class OpenAiCompatibleProvider {
|
|
|
223
259
|
const decoder = new TextDecoder();
|
|
224
260
|
let buffer = "";
|
|
225
261
|
let content = "";
|
|
262
|
+
let ended = false;
|
|
263
|
+
let finishReason;
|
|
226
264
|
const toolCallMap = new Map();
|
|
227
265
|
let usage;
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
266
|
+
const consume = (line) => {
|
|
267
|
+
const trimmed = line.trim();
|
|
268
|
+
if (!trimmed.startsWith("data:"))
|
|
269
|
+
return;
|
|
270
|
+
const data = trimmed.slice(5).trim();
|
|
271
|
+
if (!data)
|
|
272
|
+
return;
|
|
273
|
+
if (data === "[DONE]") {
|
|
274
|
+
ended = true;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const chunk = JSON.parse(data);
|
|
278
|
+
if (chunk.error) {
|
|
279
|
+
const detail = typeof chunk.error === "string" ? chunk.error : JSON.stringify(chunk.error);
|
|
280
|
+
throw new Error(`OpenAI-compatible stream error: ${this.config.apiKey ? detail.split(this.config.apiKey).join("[redacted]").slice(0, 600) : detail.slice(0, 600)}`);
|
|
281
|
+
}
|
|
282
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
283
|
+
if (typeof chunk.choices?.[0]?.finish_reason === "string")
|
|
284
|
+
finishReason = chunk.choices[0].finish_reason;
|
|
285
|
+
if (typeof delta?.content === "string") {
|
|
286
|
+
content += delta.content;
|
|
287
|
+
if (content.length > 4000000)
|
|
288
|
+
throw new Error("Model stream exceeds output limit");
|
|
289
|
+
emitToken(delta.content);
|
|
290
|
+
}
|
|
291
|
+
if (Array.isArray(delta?.tool_calls))
|
|
292
|
+
for (const call of delta.tool_calls) {
|
|
293
|
+
const index = call.index ?? 0;
|
|
294
|
+
if (!Number.isInteger(index) || index < 0 || index > 128)
|
|
295
|
+
throw new Error("Invalid streamed tool index");
|
|
296
|
+
const existing = toolCallMap.get(index) ?? { id: call.id ?? `call_${index + 1}`, name: "", args: "" };
|
|
297
|
+
if (typeof call.id === "string")
|
|
298
|
+
existing.id = call.id;
|
|
299
|
+
if (typeof call.function?.name === "string")
|
|
300
|
+
existing.name = call.function.name;
|
|
301
|
+
if (typeof call.function?.arguments === "string")
|
|
302
|
+
existing.args += call.function.arguments;
|
|
303
|
+
if (existing.args.length > 2000000)
|
|
304
|
+
throw new Error("Tool arguments exceed stream limit");
|
|
305
|
+
toolCallMap.set(index, existing);
|
|
269
306
|
}
|
|
270
|
-
|
|
271
|
-
|
|
307
|
+
if (chunk.usage)
|
|
308
|
+
usage = chunk.usage;
|
|
309
|
+
};
|
|
310
|
+
try {
|
|
311
|
+
while (true) {
|
|
312
|
+
const { value, done } = await reader.read();
|
|
313
|
+
if (done) {
|
|
314
|
+
buffer += decoder.decode();
|
|
315
|
+
if (buffer.trim())
|
|
316
|
+
consume(buffer);
|
|
317
|
+
break;
|
|
272
318
|
}
|
|
319
|
+
buffer += decoder.decode(value, { stream: true });
|
|
320
|
+
if (buffer.length > 2000000)
|
|
321
|
+
throw new Error("Model stream frame exceeds limit");
|
|
322
|
+
const lines = buffer.split("\n");
|
|
323
|
+
buffer = lines.pop() ?? "";
|
|
324
|
+
for (const line of lines)
|
|
325
|
+
consume(line);
|
|
273
326
|
}
|
|
274
327
|
}
|
|
328
|
+
finally {
|
|
329
|
+
await reader.cancel().catch(() => undefined);
|
|
330
|
+
reader.releaseLock();
|
|
331
|
+
}
|
|
332
|
+
if ((!ended && !finishReason) || finishReason === "length")
|
|
333
|
+
throw new Error("Model stream ended before a complete response");
|
|
334
|
+
if (!content && !toolCallMap.size)
|
|
335
|
+
throw new Error("Model stream returned no content or tool calls");
|
|
336
|
+
for (const call of toolCallMap.values()) {
|
|
337
|
+
if (!call.name || !call.id)
|
|
338
|
+
throw new Error("Incomplete streamed tool call");
|
|
339
|
+
JSON.parse(call.args);
|
|
340
|
+
}
|
|
275
341
|
const toolCalls = toolCallMap.size > 0
|
|
276
342
|
? Array.from(toolCallMap.entries()).map(([index, call]) => ({
|
|
277
343
|
id: call.id ?? `call_${index + 1}`,
|
|
@@ -293,7 +359,22 @@ export class OpenAiCompatibleProvider {
|
|
|
293
359
|
};
|
|
294
360
|
}
|
|
295
361
|
const raw = (await response.json());
|
|
296
|
-
|
|
362
|
+
// A relay can send HTTP 200 keepalives before inference fails. Inspect
|
|
363
|
+
// the final envelope instead of treating those headers as success.
|
|
364
|
+
if (raw?.error) {
|
|
365
|
+
const error = raw.error;
|
|
366
|
+
const record = typeof error === "object" && error !== null
|
|
367
|
+
? error
|
|
368
|
+
: {};
|
|
369
|
+
const detail = typeof error === "string" ? error
|
|
370
|
+
: typeof record.message === "string" ? record.message : "Model gateway failed";
|
|
371
|
+
const code = typeof record.code === "string" ? record.code : "upstream_error";
|
|
372
|
+
const message = `OpenAI-compatible error ${code}: ${detail}`;
|
|
373
|
+
const redacted = this.config.apiKey
|
|
374
|
+
? message.split(this.config.apiKey).join("[redacted]") : message;
|
|
375
|
+
throw new Error(redacted.slice(0, 600));
|
|
376
|
+
}
|
|
377
|
+
const choice = raw?.choices?.[0]?.message;
|
|
297
378
|
if (!choice) {
|
|
298
379
|
throw new Error("OpenAI-compatible response missing choices");
|
|
299
380
|
}
|
|
@@ -319,6 +400,7 @@ export class OpenAiCompatibleProvider {
|
|
|
319
400
|
};
|
|
320
401
|
}
|
|
321
402
|
finally {
|
|
403
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
322
404
|
clearTimeout(timeout);
|
|
323
405
|
}
|
|
324
406
|
}
|
|
@@ -43,6 +43,8 @@ export interface ProviderMessage {
|
|
|
43
43
|
content: string;
|
|
44
44
|
name?: string;
|
|
45
45
|
toolCallId?: string;
|
|
46
|
+
/** Assistant calls retained alongside the matching tool result messages. */
|
|
47
|
+
toolCalls?: ProviderToolCall[];
|
|
46
48
|
}
|
|
47
49
|
export interface ProviderUsage {
|
|
48
50
|
inputTokens?: number;
|
|
@@ -50,6 +52,7 @@ export interface ProviderUsage {
|
|
|
50
52
|
totalTokens?: number;
|
|
51
53
|
}
|
|
52
54
|
export interface ProviderRequest {
|
|
55
|
+
signal?: AbortSignal;
|
|
53
56
|
messages: ProviderMessage[];
|
|
54
57
|
tools?: ProviderToolDefinition[];
|
|
55
58
|
toolChoice?: "auto" | "none" | {
|
|
@@ -68,8 +71,19 @@ export interface ProviderResponse {
|
|
|
68
71
|
toolCalls?: ProviderToolCall[];
|
|
69
72
|
usage?: ProviderUsage;
|
|
70
73
|
raw?: unknown;
|
|
74
|
+
/** Selected configured route; host labels are configuration metadata, not hardware attestation. */
|
|
75
|
+
route?: {
|
|
76
|
+
id: string;
|
|
77
|
+
binding: string;
|
|
78
|
+
configHash: string;
|
|
79
|
+
modelId: string;
|
|
80
|
+
catalogId: string;
|
|
81
|
+
backend?: string;
|
|
82
|
+
host?: string;
|
|
83
|
+
};
|
|
71
84
|
}
|
|
72
85
|
export interface ProviderConfig {
|
|
86
|
+
retryTransient?: boolean;
|
|
73
87
|
model: string;
|
|
74
88
|
/** Agent this provider serves, so failures can name it. */
|
|
75
89
|
agentSlug?: string;
|