open-multi-agent-kit 0.90.8 → 0.90.9
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 +22 -0
- package/README.md +7 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/doctor-provider-cli.d.ts +33 -0
- package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
- package/dist/commands/doctor-provider-cli.js +215 -0
- package/dist/commands/doctor-provider-cli.js.map +1 -0
- package/dist/commands/doctor-provider-transport.d.ts +35 -0
- package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
- package/dist/commands/doctor-provider-transport.js +185 -0
- package/dist/commands/doctor-provider-transport.js.map +1 -0
- package/dist/commands/doctor-provider.d.ts +121 -7
- package/dist/commands/doctor-provider.d.ts.map +1 -1
- package/dist/commands/doctor-provider.js +1295 -102
- package/dist/commands/doctor-provider.js.map +1 -1
- package/dist/commands/session-doctor-cli.d.ts +43 -0
- package/dist/commands/session-doctor-cli.d.ts.map +1 -0
- package/dist/commands/session-doctor-cli.js +494 -0
- package/dist/commands/session-doctor-cli.js.map +1 -0
- package/dist/commands/verify-ci.d.ts +21 -0
- package/dist/commands/verify-ci.d.ts.map +1 -0
- package/dist/commands/verify-ci.js +66 -0
- package/dist/commands/verify-ci.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +113 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +993 -72
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +35 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -0
- package/dist/core/agent-tool-settings.js +155 -0
- package/dist/core/agent-tool-settings.js.map +1 -0
- package/dist/core/atomic-session-file.d.ts +14 -0
- package/dist/core/atomic-session-file.d.ts.map +1 -0
- package/dist/core/atomic-session-file.js +104 -0
- package/dist/core/atomic-session-file.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts +6 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +12 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/hysteresis.d.ts +27 -0
- package/dist/core/compaction/hysteresis.d.ts.map +1 -0
- package/dist/core/compaction/hysteresis.js +84 -0
- package/dist/core/compaction/hysteresis.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +172 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -0
- package/dist/core/compaction/transaction.js +824 -0
- package/dist/core/compaction/transaction.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +2 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +2 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
- package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
- package/dist/core/context-budget-reserved-tokens.js +188 -0
- package/dist/core/context-budget-reserved-tokens.js.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
- package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +6 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
- package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.js +19 -0
- package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +1 -0
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +4 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/durable-file-directory.d.ts +4 -0
- package/dist/core/durable-file-directory.d.ts.map +1 -0
- package/dist/core/durable-file-directory.js +76 -0
- package/dist/core/durable-file-directory.js.map +1 -0
- package/dist/core/durable-file-identity.d.ts +26 -0
- package/dist/core/durable-file-identity.d.ts.map +1 -0
- package/dist/core/durable-file-identity.js +118 -0
- package/dist/core/durable-file-identity.js.map +1 -0
- package/dist/core/durable-file-io.d.ts +29 -0
- package/dist/core/durable-file-io.d.ts.map +1 -0
- package/dist/core/durable-file-io.js +179 -0
- package/dist/core/durable-file-io.js.map +1 -0
- package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
- package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
- package/dist/core/durable-file-lock-cleanup.js +39 -0
- package/dist/core/durable-file-lock-cleanup.js.map +1 -0
- package/dist/core/durable-file-lock-owner.d.ts +18 -0
- package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
- package/dist/core/durable-file-lock-owner.js +78 -0
- package/dist/core/durable-file-lock-owner.js.map +1 -0
- package/dist/core/durable-file-lock-storage.d.ts +35 -0
- package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
- package/dist/core/durable-file-lock-storage.js +263 -0
- package/dist/core/durable-file-lock-storage.js.map +1 -0
- package/dist/core/durable-file-mode.d.ts +8 -0
- package/dist/core/durable-file-mode.d.ts.map +1 -0
- package/dist/core/durable-file-mode.js +41 -0
- package/dist/core/durable-file-mode.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +12 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +46 -37
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/index.d.ts +6 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/redaction.d.ts +8 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +31 -0
- package/dist/core/redaction.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +36 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-journal-store.d.ts +53 -0
- package/dist/core/run-journal-store.d.ts.map +1 -0
- package/dist/core/run-journal-store.js +278 -0
- package/dist/core/run-journal-store.js.map +1 -0
- package/dist/core/run-journal.d.ts +172 -0
- package/dist/core/run-journal.d.ts.map +1 -0
- package/dist/core/run-journal.js +1053 -0
- package/dist/core/run-journal.js.map +1 -0
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +94 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-doctor-plan.d.ts +128 -0
- package/dist/core/session-doctor-plan.d.ts.map +1 -0
- package/dist/core/session-doctor-plan.js +833 -0
- package/dist/core/session-doctor-plan.js.map +1 -0
- package/dist/core/session-file-compaction.d.ts +6 -0
- package/dist/core/session-file-compaction.d.ts.map +1 -0
- package/dist/core/session-file-compaction.js +169 -0
- package/dist/core/session-file-compaction.js.map +1 -0
- package/dist/core/session-file-head.d.ts +10 -0
- package/dist/core/session-file-head.d.ts.map +1 -0
- package/dist/core/session-file-head.js +78 -0
- package/dist/core/session-file-head.js.map +1 -0
- package/dist/core/session-file-persistence.d.ts +30 -0
- package/dist/core/session-file-persistence.d.ts.map +1 -0
- package/dist/core/session-file-persistence.js +106 -0
- package/dist/core/session-file-persistence.js.map +1 -0
- package/dist/core/session-integrity.d.ts +43 -0
- package/dist/core/session-integrity.d.ts.map +1 -0
- package/dist/core/session-integrity.js +420 -0
- package/dist/core/session-integrity.js.map +1 -0
- package/dist/core/session-manager.d.ts +24 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +242 -125
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-owner-lease.d.ts +22 -0
- package/dist/core/session-owner-lease.d.ts.map +1 -0
- package/dist/core/session-owner-lease.js +176 -0
- package/dist/core/session-owner-lease.js.map +1 -0
- package/dist/core/session-path-policy.d.ts +132 -0
- package/dist/core/session-path-policy.d.ts.map +1 -0
- package/dist/core/session-path-policy.js +780 -0
- package/dist/core/session-path-policy.js.map +1 -0
- package/dist/core/session-repair-plan.d.ts +49 -0
- package/dist/core/session-repair-plan.d.ts.map +1 -0
- package/dist/core/session-repair-plan.js +202 -0
- package/dist/core/session-repair-plan.js.map +1 -0
- package/dist/core/session-termination.d.ts +100 -0
- package/dist/core/session-termination.d.ts.map +1 -0
- package/dist/core/session-termination.js +403 -0
- package/dist/core/session-termination.js.map +1 -0
- package/dist/core/settings-manager.d.ts +22 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +37 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/verified-bash-adapter.d.ts +36 -0
- package/dist/core/verified-bash-adapter.d.ts.map +1 -0
- package/dist/core/verified-bash-adapter.js +90 -0
- package/dist/core/verified-bash-adapter.js.map +1 -0
- package/dist/guardrails/command-redaction.d.ts +32 -0
- package/dist/guardrails/command-redaction.d.ts.map +1 -0
- package/dist/guardrails/command-redaction.js +776 -0
- package/dist/guardrails/command-redaction.js.map +1 -0
- package/dist/guardrails/evidence-attestation.d.ts +31 -0
- package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
- package/dist/guardrails/evidence-attestation.js +178 -0
- package/dist/guardrails/evidence-attestation.js.map +1 -0
- package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
- package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt-store.js +446 -0
- package/dist/guardrails/evidence-receipt-store.js.map +1 -0
- package/dist/guardrails/evidence-receipt.d.ts +58 -0
- package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt.js +389 -0
- package/dist/guardrails/evidence-receipt.js.map +1 -0
- package/dist/guardrails/evidence-system.d.ts +39 -15
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +255 -97
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/replay-ledger-io.d.ts +6 -0
- package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-io.js +42 -0
- package/dist/guardrails/replay-ledger-io.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
- package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock.js +135 -0
- package/dist/guardrails/replay-ledger-lock.js.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.js +19 -0
- package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
- package/dist/guardrails/replay-ledger-store.d.ts +32 -0
- package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-store.js +211 -0
- package/dist/guardrails/replay-ledger-store.js.map +1 -0
- package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
- package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
- package/dist/guardrails/shell-command-name-glob.js +233 -0
- package/dist/guardrails/shell-command-name-glob.js.map +1 -0
- package/dist/guardrails/shell-command-option.d.ts +7 -0
- package/dist/guardrails/shell-command-option.d.ts.map +1 -0
- package/dist/guardrails/shell-command-option.js +24 -0
- package/dist/guardrails/shell-command-option.js.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.js +145 -0
- package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
- package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
- package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
- package/dist/guardrails/shell-command-wrapper.js +42 -0
- package/dist/guardrails/shell-command-wrapper.js.map +1 -0
- package/dist/guardrails/verified-executor.d.ts +65 -0
- package/dist/guardrails/verified-executor.d.ts.map +1 -0
- package/dist/guardrails/verified-executor.js +171 -0
- package/dist/guardrails/verified-executor.js.map +1 -0
- package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
- package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
- package/dist/guardrails/workspace-fingerprint.js +634 -0
- package/dist/guardrails/workspace-fingerprint.js.map +1 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +59 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +43 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -5
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/types/evidence.d.ts +211 -1
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/dist/verify-ci.d.ts +3 -0
- package/dist/verify-ci.d.ts.map +1 -0
- package/dist/verify-ci.js +30 -0
- package/dist/verify-ci.js.map +1 -0
- package/docs/compaction.md +38 -4
- package/docs/extensions.md +2 -0
- package/docs/sdk.md +238 -1
- package/docs/settings.md +50 -2
- package/docs/usage.md +38 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +4 -4
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -1,136 +1,1329 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { BlockList, isIP } from "node:net";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
5
|
+
import { findEnvKeys, getModels, getProviders } from "omk-ai";
|
|
6
|
+
import { getAgentDir as getConfiguredAgentDir } from "../config.js";
|
|
7
|
+
import { isConfigValueConfigured, resolveConfigValueOrThrow } from "../core/resolve-config-value.js";
|
|
8
|
+
import { stripJsonComments } from "../utils/json.js";
|
|
9
|
+
/** Fixed-message marker used to classify auth resolution without exposing its cause. */
|
|
10
|
+
export class ProviderDoctorAuthMaterializationError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("provider doctor authentication could not be materialized");
|
|
13
|
+
this.name = "ProviderDoctorAuthMaterializationError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
17
|
+
const KNOWN_APIS = new Set([
|
|
18
|
+
"openai-completions",
|
|
19
|
+
"mistral-conversations",
|
|
20
|
+
"openai-responses",
|
|
21
|
+
"azure-openai-responses",
|
|
22
|
+
"openai-codex-responses",
|
|
23
|
+
"anthropic-messages",
|
|
24
|
+
"bedrock-converse-stream",
|
|
25
|
+
"google-generative-ai",
|
|
26
|
+
"google-vertex",
|
|
27
|
+
]);
|
|
28
|
+
const OPENAI_COMPATIBLE_APIS = new Set(["openai-completions", "openai-responses"]);
|
|
29
|
+
const ABORTED = Symbol("provider-doctor-aborted");
|
|
30
|
+
// Family-specific lists: node BlockList canonicalizes IPv4 checks to the v4-mapped v6 form,
|
|
31
|
+
// so a combined list containing ::ffff:0:0/96 would block every IPv4 address.
|
|
32
|
+
const blockedPublicV4 = new BlockList();
|
|
33
|
+
for (const [address, prefix] of [
|
|
34
|
+
["0.0.0.0", 8],
|
|
35
|
+
["10.0.0.0", 8],
|
|
36
|
+
["100.64.0.0", 10],
|
|
37
|
+
["127.0.0.0", 8],
|
|
38
|
+
["169.254.0.0", 16],
|
|
39
|
+
["172.16.0.0", 12],
|
|
40
|
+
["192.168.0.0", 16],
|
|
41
|
+
["224.0.0.0", 4],
|
|
42
|
+
["240.0.0.0", 4],
|
|
43
|
+
]) {
|
|
44
|
+
blockedPublicV4.addSubnet(address, prefix, "ipv4");
|
|
45
|
+
}
|
|
46
|
+
// ::ffff:0:0/96 stays here on purpose: a v4-mapped IPv6 literal or DNS answer is anomalous
|
|
47
|
+
// for a public origin and is rejected wholesale (fail closed), covering hex-form mappings too.
|
|
48
|
+
const blockedPublicV6 = new BlockList();
|
|
49
|
+
for (const [address, prefix] of [
|
|
50
|
+
["::", 128],
|
|
51
|
+
["::1", 128],
|
|
52
|
+
["::ffff:0:0", 96],
|
|
53
|
+
["fc00::", 7],
|
|
54
|
+
["fe80::", 10],
|
|
55
|
+
["ff00::", 8],
|
|
56
|
+
]) {
|
|
57
|
+
blockedPublicV6.addSubnet(address, prefix, "ipv6");
|
|
58
|
+
}
|
|
59
|
+
const loopbackAddresses = new BlockList();
|
|
60
|
+
loopbackAddresses.addSubnet("127.0.0.0", 8, "ipv4");
|
|
61
|
+
loopbackAddresses.addAddress("::1", "ipv6");
|
|
62
|
+
loopbackAddresses.addSubnet("::ffff:127.0.0.0", 104, "ipv6");
|
|
63
|
+
/** True when the literal IP address is a loopback address. Non-IP input is never loopback. */
|
|
64
|
+
export function isLoopbackAddress(address) {
|
|
65
|
+
const family = isIP(address);
|
|
66
|
+
if (family === 4)
|
|
67
|
+
return loopbackAddresses.check(address, "ipv4");
|
|
68
|
+
if (family === 6)
|
|
69
|
+
return loopbackAddresses.check(address, "ipv6");
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/** True when the literal IP address must not be dialed for a public origin. Non-IP input fails closed. */
|
|
73
|
+
export function isBlockedPublicAddress(address) {
|
|
74
|
+
const family = isIP(address);
|
|
75
|
+
if (family === 4)
|
|
76
|
+
return blockedPublicV4.check(address, "ipv4");
|
|
77
|
+
if (family === 6)
|
|
78
|
+
return blockedPublicV6.check(address, "ipv6");
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
4
81
|
export function getAgentDir() {
|
|
5
|
-
return
|
|
82
|
+
return getConfiguredAgentDir();
|
|
6
83
|
}
|
|
7
|
-
function
|
|
8
|
-
|
|
84
|
+
function isRecord(value) {
|
|
85
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
function isOptionalNonemptyString(value) {
|
|
88
|
+
return value === undefined || (typeof value === "string" && value.length > 0);
|
|
89
|
+
}
|
|
90
|
+
function parseModelsConfig(value) {
|
|
91
|
+
if (!isRecord(value) || !isRecord(value.providers))
|
|
92
|
+
return undefined;
|
|
93
|
+
const providers = {};
|
|
94
|
+
for (const [providerId, rawProvider] of Object.entries(value.providers)) {
|
|
95
|
+
if (!isRecord(rawProvider))
|
|
96
|
+
return undefined;
|
|
97
|
+
if (!isOptionalNonemptyString(rawProvider.baseUrl) ||
|
|
98
|
+
!isOptionalNonemptyString(rawProvider.apiKey) ||
|
|
99
|
+
!isOptionalNonemptyString(rawProvider.api)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
let headers;
|
|
103
|
+
if (rawProvider.headers !== undefined) {
|
|
104
|
+
if (!isRecord(rawProvider.headers))
|
|
105
|
+
return undefined;
|
|
106
|
+
headers = {};
|
|
107
|
+
for (const [name, headerValue] of Object.entries(rawProvider.headers)) {
|
|
108
|
+
if (typeof headerValue !== "string")
|
|
109
|
+
return undefined;
|
|
110
|
+
headers[name] = headerValue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let models;
|
|
114
|
+
if (rawProvider.models !== undefined) {
|
|
115
|
+
if (!Array.isArray(rawProvider.models))
|
|
116
|
+
return undefined;
|
|
117
|
+
models = [];
|
|
118
|
+
for (const rawModel of rawProvider.models) {
|
|
119
|
+
if (!isRecord(rawModel) ||
|
|
120
|
+
typeof rawModel.id !== "string" ||
|
|
121
|
+
rawModel.id.length === 0 ||
|
|
122
|
+
!isOptionalNonemptyString(rawModel.baseUrl) ||
|
|
123
|
+
!isOptionalNonemptyString(rawModel.api)) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
models.push({ id: rawModel.id, baseUrl: rawModel.baseUrl, api: rawModel.api });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
providers[providerId] = {
|
|
130
|
+
baseUrl: rawProvider.baseUrl,
|
|
131
|
+
apiKey: rawProvider.apiKey,
|
|
132
|
+
api: rawProvider.api,
|
|
133
|
+
headers,
|
|
134
|
+
models,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { providers };
|
|
138
|
+
}
|
|
139
|
+
function parseAuthConfig(value) {
|
|
140
|
+
if (!isRecord(value))
|
|
9
141
|
return undefined;
|
|
142
|
+
const auth = {};
|
|
143
|
+
for (const [providerId, rawEntry] of Object.entries(value)) {
|
|
144
|
+
if (!isRecord(rawEntry) || (rawEntry.type !== "api_key" && rawEntry.type !== "oauth"))
|
|
145
|
+
return undefined;
|
|
146
|
+
if (rawEntry.type === "api_key" && (typeof rawEntry.key !== "string" || rawEntry.key.length === 0)) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
auth[providerId] = { type: rawEntry.type };
|
|
150
|
+
}
|
|
151
|
+
return auth;
|
|
152
|
+
}
|
|
153
|
+
const KIMI_PROVIDER_ID = "kimi-coding";
|
|
154
|
+
const TOML_BASIC_ESCAPES = {
|
|
155
|
+
'"': '"',
|
|
156
|
+
"\\": "\\",
|
|
157
|
+
b: "\b",
|
|
158
|
+
f: "\f",
|
|
159
|
+
n: "\n",
|
|
160
|
+
r: "\r",
|
|
161
|
+
t: "\t",
|
|
162
|
+
};
|
|
163
|
+
function findBasicStringEnd(text, start, multiline) {
|
|
164
|
+
for (let index = start; index < text.length; index++) {
|
|
165
|
+
if (text[index] === "\\") {
|
|
166
|
+
index++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (multiline ? text.startsWith('"""', index) : text[index] === '"')
|
|
170
|
+
return index;
|
|
171
|
+
}
|
|
172
|
+
return -1;
|
|
173
|
+
}
|
|
174
|
+
function decodeTomlBasicString(body, multiline) {
|
|
175
|
+
let value = "";
|
|
176
|
+
for (let index = 0; index < body.length; index++) {
|
|
177
|
+
const char = body[index];
|
|
178
|
+
if (char !== "\\") {
|
|
179
|
+
const code = char.codePointAt(0) ?? 0;
|
|
180
|
+
if (code < 0x20 && char !== "\t" && !(multiline && (char === "\n" || char === "\r"))) {
|
|
181
|
+
return { error: "invalid control character in string value" };
|
|
182
|
+
}
|
|
183
|
+
value += char;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const escapeChar = body[index + 1];
|
|
187
|
+
if (escapeChar === undefined)
|
|
188
|
+
return { error: "unterminated escape sequence" };
|
|
189
|
+
if (multiline && (escapeChar === "\n" || escapeChar === "\r")) {
|
|
190
|
+
index++;
|
|
191
|
+
while (index + 1 < body.length && /\s/.test(body[index + 1]))
|
|
192
|
+
index++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (escapeChar === "u" || escapeChar === "U") {
|
|
196
|
+
const width = escapeChar === "u" ? 4 : 8;
|
|
197
|
+
const hex = body.slice(index + 2, index + 2 + width);
|
|
198
|
+
const codePoint = /^[0-9a-fA-F]+$/.test(hex) && hex.length === width ? Number.parseInt(hex, 16) : -1;
|
|
199
|
+
if (codePoint < 0 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
|
|
200
|
+
return { error: "invalid unicode escape" };
|
|
201
|
+
}
|
|
202
|
+
value += String.fromCodePoint(codePoint);
|
|
203
|
+
index += 1 + width;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const mapped = TOML_BASIC_ESCAPES[escapeChar];
|
|
207
|
+
if (mapped === undefined)
|
|
208
|
+
return { error: "invalid escape sequence" };
|
|
209
|
+
value += mapped;
|
|
210
|
+
index++;
|
|
211
|
+
}
|
|
212
|
+
return { value };
|
|
213
|
+
}
|
|
214
|
+
/** Parses one TOML string value. Errors are fixed and never include raw TOML content. */
|
|
215
|
+
function parseTomlStringValue(raw, requireNonempty = true) {
|
|
216
|
+
const text = raw.trim();
|
|
217
|
+
const multilineBasic = text.startsWith('"""');
|
|
218
|
+
const multilineLiteral = text.startsWith("'''");
|
|
219
|
+
const basic = !multilineBasic && text.startsWith('"');
|
|
220
|
+
const literal = !multilineLiteral && text.startsWith("'");
|
|
221
|
+
if (!multilineBasic && !multilineLiteral && !basic && !literal) {
|
|
222
|
+
return { error: "value must be a quoted string" };
|
|
223
|
+
}
|
|
224
|
+
const delimiterWidth = multilineBasic || multilineLiteral ? 3 : 1;
|
|
225
|
+
const end = multilineBasic || basic
|
|
226
|
+
? findBasicStringEnd(text, delimiterWidth, multilineBasic)
|
|
227
|
+
: text.indexOf(multilineLiteral ? "'''" : "'", delimiterWidth);
|
|
228
|
+
if (end < 0) {
|
|
229
|
+
return {
|
|
230
|
+
error: multilineBasic || multilineLiteral ? "unterminated multiline string value" : "unterminated string value",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (text.slice(end + delimiterWidth).trim().length > 0) {
|
|
234
|
+
return { error: "unexpected content after string value" };
|
|
235
|
+
}
|
|
236
|
+
let body = text.slice(delimiterWidth, end);
|
|
237
|
+
if ((multilineBasic || multilineLiteral) && body.startsWith("\n"))
|
|
238
|
+
body = body.slice(1);
|
|
239
|
+
const parsed = multilineBasic || basic
|
|
240
|
+
? decodeTomlBasicString(body, multilineBasic)
|
|
241
|
+
: /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(body)
|
|
242
|
+
? { error: "invalid control character in string value" }
|
|
243
|
+
: { value: body };
|
|
244
|
+
if ("error" in parsed)
|
|
245
|
+
return parsed;
|
|
246
|
+
return requireNonempty && parsed.value.length === 0 ? { error: "empty string value" } : parsed;
|
|
247
|
+
}
|
|
248
|
+
function scanTomlLine(text, state) {
|
|
249
|
+
let content = "";
|
|
250
|
+
for (let index = 0; index < text.length; index++) {
|
|
251
|
+
const char = text[index];
|
|
252
|
+
if (state.quote === "basic") {
|
|
253
|
+
content += char;
|
|
254
|
+
if (char === "\\" && index + 1 < text.length)
|
|
255
|
+
content += text[++index];
|
|
256
|
+
else if (char === '"')
|
|
257
|
+
state.quote = undefined;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (state.quote === "literal") {
|
|
261
|
+
content += char;
|
|
262
|
+
if (char === "'")
|
|
263
|
+
state.quote = undefined;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (state.quote === "multiline-basic") {
|
|
267
|
+
if (text.startsWith('"""', index)) {
|
|
268
|
+
content += '"""';
|
|
269
|
+
index += 2;
|
|
270
|
+
state.quote = undefined;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
content += char;
|
|
274
|
+
if (char === "\\" && index + 1 < text.length)
|
|
275
|
+
content += text[++index];
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (state.quote === "multiline-literal") {
|
|
280
|
+
if (text.startsWith("'''", index)) {
|
|
281
|
+
content += "'''";
|
|
282
|
+
index += 2;
|
|
283
|
+
state.quote = undefined;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
content += char;
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (char === "#")
|
|
291
|
+
break;
|
|
292
|
+
if (text.startsWith('"""', index)) {
|
|
293
|
+
content += '"""';
|
|
294
|
+
index += 2;
|
|
295
|
+
state.quote = "multiline-basic";
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (text.startsWith("'''", index)) {
|
|
299
|
+
content += "'''";
|
|
300
|
+
index += 2;
|
|
301
|
+
state.quote = "multiline-literal";
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
content += char;
|
|
305
|
+
if (char === '"')
|
|
306
|
+
state.quote = "basic";
|
|
307
|
+
else if (char === "'")
|
|
308
|
+
state.quote = "literal";
|
|
309
|
+
else if (char === "[" || char === "{")
|
|
310
|
+
state.containers.push(char);
|
|
311
|
+
else if (char === "]" || char === "}") {
|
|
312
|
+
const expected = char === "]" ? "[" : "{";
|
|
313
|
+
if (state.containers.pop() !== expected)
|
|
314
|
+
return { content, error: "mismatched value delimiter" };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (state.quote === "basic" || state.quote === "literal") {
|
|
318
|
+
return { content, error: "unterminated string value" };
|
|
319
|
+
}
|
|
320
|
+
return { content };
|
|
321
|
+
}
|
|
322
|
+
function consumeTomlValue(lines, startLine, initial) {
|
|
323
|
+
const state = { containers: [] };
|
|
324
|
+
const chunks = [];
|
|
325
|
+
for (let line = startLine; line < lines.length; line++) {
|
|
326
|
+
const scanned = scanTomlLine(line === startLine ? initial : lines[line], state);
|
|
327
|
+
chunks.push(scanned.content);
|
|
328
|
+
if (scanned.error)
|
|
329
|
+
return { endLine: line, error: scanned.error };
|
|
330
|
+
if (!state.quote && state.containers.length === 0) {
|
|
331
|
+
const value = chunks.join("\n").trim();
|
|
332
|
+
return value.length > 0
|
|
333
|
+
? { value, endLine: line }
|
|
334
|
+
: { endLine: startLine, error: "assignment value is missing" };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
endLine: startLine,
|
|
339
|
+
error: state.quote ? "unterminated multiline string value" : "unterminated compound value",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function findTomlTopLevelCharacter(text, wanted) {
|
|
343
|
+
const state = { containers: [] };
|
|
344
|
+
for (let index = 0; index < text.length; index++) {
|
|
345
|
+
const char = text[index];
|
|
346
|
+
if (state.quote === "basic" || state.quote === "multiline-basic") {
|
|
347
|
+
if (char === "\\")
|
|
348
|
+
index++;
|
|
349
|
+
else if (state.quote === "basic" && char === '"')
|
|
350
|
+
state.quote = undefined;
|
|
351
|
+
else if (state.quote === "multiline-basic" && text.startsWith('"""', index)) {
|
|
352
|
+
state.quote = undefined;
|
|
353
|
+
index += 2;
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (state.quote === "literal" || state.quote === "multiline-literal") {
|
|
358
|
+
const delimiter = state.quote === "literal" ? "'" : "'''";
|
|
359
|
+
if (text.startsWith(delimiter, index)) {
|
|
360
|
+
state.quote = undefined;
|
|
361
|
+
index += delimiter.length - 1;
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (char === "#")
|
|
366
|
+
return -1;
|
|
367
|
+
if (text.startsWith('"""', index)) {
|
|
368
|
+
state.quote = "multiline-basic";
|
|
369
|
+
index += 2;
|
|
370
|
+
}
|
|
371
|
+
else if (text.startsWith("'''", index)) {
|
|
372
|
+
state.quote = "multiline-literal";
|
|
373
|
+
index += 2;
|
|
374
|
+
}
|
|
375
|
+
else if (char === '"')
|
|
376
|
+
state.quote = "basic";
|
|
377
|
+
else if (char === "'")
|
|
378
|
+
state.quote = "literal";
|
|
379
|
+
else if (char === "[" || char === "{")
|
|
380
|
+
state.containers.push(char);
|
|
381
|
+
else if (char === "]" || char === "}")
|
|
382
|
+
state.containers.pop();
|
|
383
|
+
else if (char === wanted && state.containers.length === 0)
|
|
384
|
+
return index;
|
|
385
|
+
}
|
|
386
|
+
return -1;
|
|
387
|
+
}
|
|
388
|
+
function parseTomlDottedKey(raw) {
|
|
389
|
+
const components = [];
|
|
390
|
+
let index = 0;
|
|
391
|
+
while (index < raw.length) {
|
|
392
|
+
while (/\s/.test(raw[index] ?? ""))
|
|
393
|
+
index++;
|
|
394
|
+
let component;
|
|
395
|
+
if (raw[index] === '"') {
|
|
396
|
+
const end = findBasicStringEnd(raw, index + 1, false);
|
|
397
|
+
if (end < 0)
|
|
398
|
+
return undefined;
|
|
399
|
+
const parsed = parseTomlStringValue(raw.slice(index, end + 1), false);
|
|
400
|
+
if ("error" in parsed)
|
|
401
|
+
return undefined;
|
|
402
|
+
component = parsed.value;
|
|
403
|
+
index = end + 1;
|
|
404
|
+
}
|
|
405
|
+
else if (raw[index] === "'") {
|
|
406
|
+
const end = raw.indexOf("'", index + 1);
|
|
407
|
+
if (end < 0)
|
|
408
|
+
return undefined;
|
|
409
|
+
component = raw.slice(index + 1, end);
|
|
410
|
+
index = end + 1;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const match = /^[A-Za-z0-9_-]+/.exec(raw.slice(index));
|
|
414
|
+
if (!match)
|
|
415
|
+
return undefined;
|
|
416
|
+
component = match[0];
|
|
417
|
+
index += match[0].length;
|
|
418
|
+
}
|
|
419
|
+
components.push(component);
|
|
420
|
+
while (/\s/.test(raw[index] ?? ""))
|
|
421
|
+
index++;
|
|
422
|
+
if (index === raw.length)
|
|
423
|
+
return components;
|
|
424
|
+
if (raw[index] !== ".")
|
|
425
|
+
return undefined;
|
|
426
|
+
index++;
|
|
427
|
+
if (index === raw.length)
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
function splitTomlTopLevel(raw) {
|
|
433
|
+
const parts = [];
|
|
434
|
+
let start = 0;
|
|
435
|
+
for (let index = 0; index < raw.length; index++) {
|
|
436
|
+
if (raw[index] !== ",")
|
|
437
|
+
continue;
|
|
438
|
+
const relative = findTomlTopLevelCharacter(raw.slice(start, index + 1), ",");
|
|
439
|
+
if (relative !== index - start)
|
|
440
|
+
continue;
|
|
441
|
+
parts.push(raw.slice(start, index).trim());
|
|
442
|
+
start = index + 1;
|
|
443
|
+
}
|
|
444
|
+
parts.push(raw.slice(start).trim());
|
|
445
|
+
return parts;
|
|
446
|
+
}
|
|
447
|
+
function isValidTomlScalar(raw) {
|
|
448
|
+
return (raw === "true" ||
|
|
449
|
+
raw === "false" ||
|
|
450
|
+
/^[+-]?(?:0|[1-9](?:_?\d)*)$/.test(raw) ||
|
|
451
|
+
/^[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*$/.test(raw) ||
|
|
452
|
+
/^[+-]?0o[0-7](?:_?[0-7])*$/.test(raw) ||
|
|
453
|
+
/^[+-]?0b[01](?:_?[01])*$/.test(raw) ||
|
|
454
|
+
/^[+-]?(?:(?:0|[1-9](?:_?\d)*)\.(?:\d(?:_?\d)*)(?:[eE][+-]?\d(?:_?\d)*)?|(?:0|[1-9](?:_?\d)*)[eE][+-]?\d(?:_?\d)*|inf|nan)$/.test(raw) ||
|
|
455
|
+
/^\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})?)?$/.test(raw) ||
|
|
456
|
+
/^\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(raw));
|
|
457
|
+
}
|
|
458
|
+
function isValidTomlValue(raw) {
|
|
459
|
+
const text = raw.trim();
|
|
460
|
+
if (text.startsWith('"') || text.startsWith("'"))
|
|
461
|
+
return !("error" in parseTomlStringValue(text, false));
|
|
462
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
463
|
+
const parts = splitTomlTopLevel(text.slice(1, -1));
|
|
464
|
+
if (parts.at(-1) === "")
|
|
465
|
+
parts.pop();
|
|
466
|
+
return parts.every((part) => part.length > 0 && isValidTomlValue(part));
|
|
467
|
+
}
|
|
468
|
+
if (text.startsWith("{") && text.endsWith("}")) {
|
|
469
|
+
const body = text.slice(1, -1).trim();
|
|
470
|
+
if (body.length === 0)
|
|
471
|
+
return true;
|
|
472
|
+
return splitTomlTopLevel(body).every((part) => {
|
|
473
|
+
const equals = findTomlTopLevelCharacter(part, "=");
|
|
474
|
+
return equals > 0 && !!parseTomlDottedKey(part.slice(0, equals)) && isValidTomlValue(part.slice(equals + 1));
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return isValidTomlScalar(text);
|
|
478
|
+
}
|
|
479
|
+
function parseTomlTable(line) {
|
|
480
|
+
const state = { containers: [] };
|
|
481
|
+
const scanned = scanTomlLine(line, state);
|
|
482
|
+
if (scanned.error || state.quote || state.containers.length > 0)
|
|
483
|
+
return { error: "malformed table header" };
|
|
484
|
+
const text = scanned.content.trim();
|
|
485
|
+
const array = text.startsWith("[[");
|
|
486
|
+
if (array ? !text.endsWith("]]") : !text.endsWith("]"))
|
|
487
|
+
return { error: "malformed table header" };
|
|
488
|
+
const inner = text.slice(array ? 2 : 1, array ? -2 : -1).trim();
|
|
489
|
+
const components = parseTomlDottedKey(inner);
|
|
490
|
+
return components && components.length > 0 ? { components, array } : { error: "malformed table header" };
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Strict Kimi TOML subset reader. It validates every table/assignment and generic TOML value,
|
|
494
|
+
* but extracts only root `base_url`/`model_name` and `[providers.<name>]` string fields. Fixed
|
|
495
|
+
* line-number errors ensure credentials and other file content are never echoed.
|
|
496
|
+
*/
|
|
497
|
+
function loadKimiConfig(path) {
|
|
498
|
+
if (!existsSync(path))
|
|
499
|
+
return { kind: "missing" };
|
|
500
|
+
let content;
|
|
10
501
|
try {
|
|
11
|
-
|
|
502
|
+
content = readFileSync(path, "utf-8");
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return { kind: "invalid", line: 0, reason: "file is unreadable" };
|
|
506
|
+
}
|
|
507
|
+
const value = { providers: [] };
|
|
508
|
+
const lines = content.split(/\r?\n/);
|
|
509
|
+
const assignments = new Set();
|
|
510
|
+
let table = "root";
|
|
511
|
+
let scope = "root";
|
|
512
|
+
for (let index = 0; index < lines.length; index++) {
|
|
513
|
+
const line = lines[index].trim();
|
|
514
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
515
|
+
continue;
|
|
516
|
+
if (line.startsWith("[")) {
|
|
517
|
+
const parsed = parseTomlTable(line);
|
|
518
|
+
if (parsed.error || !parsed.components) {
|
|
519
|
+
return { kind: "invalid", line: index + 1, reason: parsed.error ?? "malformed table header" };
|
|
520
|
+
}
|
|
521
|
+
const [first, name] = parsed.components;
|
|
522
|
+
if (first === "providers" && parsed.array) {
|
|
523
|
+
return { kind: "invalid", line: index + 1, reason: "provider table cannot be an array" };
|
|
524
|
+
}
|
|
525
|
+
if (first === "providers" && parsed.components.length === 2 && name !== undefined && name.length > 0) {
|
|
526
|
+
const entry = { name };
|
|
527
|
+
value.providers.push(entry);
|
|
528
|
+
table = entry;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
table = "other";
|
|
532
|
+
}
|
|
533
|
+
scope = `${parsed.array ? "array" : "table"}:${JSON.stringify(parsed.components)}`;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const assignmentLine = index;
|
|
537
|
+
const equals = findTomlTopLevelCharacter(line, "=");
|
|
538
|
+
const key = equals > 0 ? parseTomlDottedKey(line.slice(0, equals)) : undefined;
|
|
539
|
+
if (!key || key.length === 0)
|
|
540
|
+
return { kind: "invalid", line: index + 1, reason: "invalid assignment" };
|
|
541
|
+
const consumed = consumeTomlValue(lines, index, line.slice(equals + 1));
|
|
542
|
+
if (consumed.error || consumed.value === undefined) {
|
|
543
|
+
return { kind: "invalid", line: assignmentLine + 1, reason: consumed.error ?? "invalid assignment value" };
|
|
544
|
+
}
|
|
545
|
+
index = consumed.endLine;
|
|
546
|
+
if (!isValidTomlValue(consumed.value)) {
|
|
547
|
+
return { kind: "invalid", line: assignmentLine + 1, reason: "invalid assignment value" };
|
|
548
|
+
}
|
|
549
|
+
const assignmentId = `${scope}:${JSON.stringify(key)}`;
|
|
550
|
+
if (assignments.has(assignmentId)) {
|
|
551
|
+
return { kind: "invalid", line: assignmentLine + 1, reason: "duplicate assignment" };
|
|
552
|
+
}
|
|
553
|
+
assignments.add(assignmentId);
|
|
554
|
+
if (key.length !== 1 || table === "other")
|
|
555
|
+
continue;
|
|
556
|
+
const keyName = key[0];
|
|
557
|
+
const relevant = table === "root"
|
|
558
|
+
? keyName === "base_url" || keyName === "model_name"
|
|
559
|
+
: keyName === "base_url" ||
|
|
560
|
+
keyName === "model_name" ||
|
|
561
|
+
keyName === "model" ||
|
|
562
|
+
keyName === "type" ||
|
|
563
|
+
keyName === "api_key";
|
|
564
|
+
if (!relevant)
|
|
565
|
+
continue;
|
|
566
|
+
const parsedValue = parseTomlStringValue(consumed.value);
|
|
567
|
+
if ("error" in parsedValue)
|
|
568
|
+
return { kind: "invalid", line: assignmentLine + 1, reason: parsedValue.error };
|
|
569
|
+
if (table === "root") {
|
|
570
|
+
if (keyName === "base_url")
|
|
571
|
+
value.baseUrl = parsedValue.value;
|
|
572
|
+
else
|
|
573
|
+
value.modelName = parsedValue.value;
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
if (keyName === "base_url")
|
|
577
|
+
table.baseUrl = parsedValue.value;
|
|
578
|
+
else if (keyName === "type")
|
|
579
|
+
table.type = parsedValue.value;
|
|
580
|
+
else if (keyName === "api_key")
|
|
581
|
+
table.apiKey = parsedValue.value;
|
|
582
|
+
else
|
|
583
|
+
table.modelName = parsedValue.value;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return { kind: "ok", value };
|
|
587
|
+
}
|
|
588
|
+
function nonemptyEnv(name) {
|
|
589
|
+
const value = process.env[name];
|
|
590
|
+
return value && value.length > 0 ? value : undefined;
|
|
591
|
+
}
|
|
592
|
+
function resolveKimiConfig(kimiConfigPath) {
|
|
593
|
+
const path = kimiConfigPath ?? join(homedir(), ".kimi", "config.toml");
|
|
594
|
+
const loaded = loadKimiConfig(path);
|
|
595
|
+
if (loaded.kind === "invalid")
|
|
596
|
+
return { failure: { line: loaded.line, reason: loaded.reason } };
|
|
597
|
+
const fileValues = loaded.kind === "ok" ? loaded.value : { providers: [] };
|
|
598
|
+
const envBaseUrl = nonemptyEnv("KIMI_BASE_URL");
|
|
599
|
+
const envModelName = nonemptyEnv("KIMI_MODEL_NAME");
|
|
600
|
+
const custom = fileValues.providers.find((entry) => entry.type === "openai_legacy" && entry.baseUrl);
|
|
601
|
+
return {
|
|
602
|
+
baseUrl: envBaseUrl ?? fileValues.baseUrl,
|
|
603
|
+
modelName: envModelName ?? fileValues.modelName,
|
|
604
|
+
baseUrlSource: envBaseUrl ? "kimi-environment" : fileValues.baseUrl ? "kimi-config-toml" : undefined,
|
|
605
|
+
customProvider: custom
|
|
606
|
+
? { name: custom.name, baseUrl: custom.baseUrl, modelName: custom.modelName, apiKey: custom.apiKey }
|
|
607
|
+
: undefined,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function loadConfig(path, parse, allowComments = false) {
|
|
611
|
+
if (!existsSync(path))
|
|
612
|
+
return { kind: "missing" };
|
|
613
|
+
try {
|
|
614
|
+
const content = readFileSync(path, "utf-8");
|
|
615
|
+
const parsed = JSON.parse(allowComments ? stripJsonComments(content) : content);
|
|
616
|
+
const value = parse(parsed);
|
|
617
|
+
return value ? { kind: "ok", value } : { kind: "invalid" };
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
return { kind: "invalid" };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
export function normalizedUrlHostname(url) {
|
|
624
|
+
return normalizedHostname(url);
|
|
625
|
+
}
|
|
626
|
+
function normalizedHostname(url) {
|
|
627
|
+
const hostname = url.hostname.toLowerCase();
|
|
628
|
+
const unwrapped = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
629
|
+
return unwrapped.endsWith(".") ? unwrapped.slice(0, -1) : unwrapped;
|
|
630
|
+
}
|
|
631
|
+
function isLiteralLoopback(hostname) {
|
|
632
|
+
if (hostname === "localhost" || hostname === "::1")
|
|
633
|
+
return true;
|
|
634
|
+
if (isIP(hostname) !== 4)
|
|
635
|
+
return false;
|
|
636
|
+
return Number(hostname.split(".")[0]) === 127;
|
|
637
|
+
}
|
|
638
|
+
function isBlockedPublicHost(hostname) {
|
|
639
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
640
|
+
return true;
|
|
641
|
+
if (isIP(hostname) === 0)
|
|
642
|
+
return false;
|
|
643
|
+
return isBlockedPublicAddress(hostname);
|
|
644
|
+
}
|
|
645
|
+
function sanitizeUrl(rawUrl) {
|
|
646
|
+
try {
|
|
647
|
+
const url = new URL(rawUrl);
|
|
648
|
+
url.username = "";
|
|
649
|
+
url.password = "";
|
|
650
|
+
url.search = "";
|
|
651
|
+
url.hash = "";
|
|
652
|
+
return url;
|
|
12
653
|
}
|
|
13
654
|
catch {
|
|
14
655
|
return undefined;
|
|
15
656
|
}
|
|
16
657
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
658
|
+
export function canonicalProviderEndpointUrl(rawUrl) {
|
|
659
|
+
if (!rawUrl)
|
|
660
|
+
return undefined;
|
|
661
|
+
const url = sanitizeUrl(rawUrl);
|
|
662
|
+
if (!url)
|
|
663
|
+
return undefined;
|
|
664
|
+
const hostname = normalizedHostname(url);
|
|
665
|
+
const host = isIP(hostname) === 6 ? `[${hostname}]` : hostname;
|
|
666
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
667
|
+
return `${url.protocol}//${host}${url.port ? `:${url.port}` : ""}${pathname}`;
|
|
668
|
+
}
|
|
669
|
+
function credentialBindingMatchesTarget(binding, target) {
|
|
670
|
+
return (binding.origin === target.origin &&
|
|
671
|
+
binding.source === target.source &&
|
|
672
|
+
binding.baseUrl === canonicalProviderEndpointUrl(target.endpoint.baseUrl) &&
|
|
673
|
+
binding.api === target.endpoint.api &&
|
|
674
|
+
binding.modelId === target.endpoint.modelId);
|
|
675
|
+
}
|
|
676
|
+
function appendPath(url, segment) {
|
|
677
|
+
const next = new URL(url.href);
|
|
678
|
+
next.pathname = `${next.pathname.replace(/\/$/, "")}/${segment}`;
|
|
679
|
+
return next;
|
|
680
|
+
}
|
|
681
|
+
function makeRootProbeUrl(url, origin) {
|
|
682
|
+
if (origin !== "local-proxy")
|
|
683
|
+
return new URL(url.href);
|
|
684
|
+
const health = new URL(url.href);
|
|
685
|
+
health.pathname = "/health";
|
|
686
|
+
return health;
|
|
687
|
+
}
|
|
688
|
+
function publicResult(target, level, checks, url, failure, costWarning) {
|
|
689
|
+
const targetUrl = url?.href;
|
|
690
|
+
return {
|
|
691
|
+
provider: target.providerId,
|
|
692
|
+
status: failure ? "fail" : "ok",
|
|
693
|
+
level,
|
|
694
|
+
origin: target.origin,
|
|
695
|
+
source: target.source,
|
|
696
|
+
targetUrl,
|
|
697
|
+
baseUrl: targetUrl,
|
|
698
|
+
api: target.endpoint.api,
|
|
699
|
+
modelId: target.endpoint.modelId,
|
|
700
|
+
authPresent: target.auth.present,
|
|
701
|
+
...(costWarning ? { costWarning: true } : {}),
|
|
702
|
+
checks,
|
|
703
|
+
error: failure,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function unresolvedFailure(providerId, level, code, message) {
|
|
707
|
+
const check = { name: "config", status: "fail", category: "config", code, message };
|
|
708
|
+
return {
|
|
709
|
+
provider: providerId,
|
|
710
|
+
status: "fail",
|
|
711
|
+
level,
|
|
712
|
+
origin: "unknown",
|
|
713
|
+
source: "unresolved",
|
|
714
|
+
authPresent: false,
|
|
715
|
+
checks: [check],
|
|
716
|
+
error: { category: "config", code, message },
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function staticChecks(target) {
|
|
720
|
+
const checks = [
|
|
721
|
+
{ name: "config-present", status: "ok", code: "ok", message: "Provider configuration resolved" },
|
|
722
|
+
];
|
|
723
|
+
if (target.origin === "custom-openai-compatible") {
|
|
724
|
+
checks.push({
|
|
725
|
+
name: "native-provider-checks",
|
|
726
|
+
status: "skipped",
|
|
727
|
+
code: "ok",
|
|
728
|
+
message: "Custom OpenAI-compatible endpoint; native-only provider checks skipped",
|
|
729
|
+
});
|
|
23
730
|
}
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
731
|
+
let failure;
|
|
732
|
+
const recordFailure = (next) => {
|
|
733
|
+
failure ??= next;
|
|
734
|
+
};
|
|
735
|
+
const requestedModel = target.endpoint.modelId;
|
|
736
|
+
const modelIds = target.endpoint.modelIds ?? [];
|
|
737
|
+
if (!requestedModel) {
|
|
738
|
+
checks.push({ name: "model-provider-relation", status: "skipped", code: "ok", message: "No model selected" });
|
|
739
|
+
}
|
|
740
|
+
else if (!modelIds.includes(requestedModel)) {
|
|
741
|
+
const next = {
|
|
742
|
+
category: "model",
|
|
743
|
+
code: "model-provider-mismatch",
|
|
744
|
+
message: "Selected model does not belong to the provider",
|
|
745
|
+
};
|
|
746
|
+
checks.push({ name: "model-provider-relation", status: "fail", ...next });
|
|
747
|
+
recordFailure(next);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
checks.push({ name: "model-provider-relation", status: "ok", code: "ok", message: "Model belongs to provider" });
|
|
751
|
+
}
|
|
752
|
+
const api = target.endpoint.api;
|
|
753
|
+
if (!api) {
|
|
754
|
+
const next = { category: "config", code: "api-missing", message: "Provider API type is missing" };
|
|
755
|
+
checks.push({ name: "api", status: "fail", ...next });
|
|
756
|
+
recordFailure(next);
|
|
757
|
+
}
|
|
758
|
+
else if (!KNOWN_APIS.has(api) ||
|
|
759
|
+
(target.origin === "custom-openai-compatible" && !OPENAI_COMPATIBLE_APIS.has(api))) {
|
|
760
|
+
const next = {
|
|
761
|
+
category: "config",
|
|
762
|
+
code: "api-unsupported",
|
|
763
|
+
message: "Provider API type is unsupported",
|
|
764
|
+
};
|
|
765
|
+
checks.push({ name: "api", status: "fail", ...next });
|
|
766
|
+
recordFailure(next);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
checks.push({ name: "api", status: "ok", code: "ok", message: "Provider API type is supported" });
|
|
770
|
+
}
|
|
771
|
+
let url;
|
|
772
|
+
if (!target.endpoint.baseUrl) {
|
|
773
|
+
const next = {
|
|
774
|
+
category: "config",
|
|
775
|
+
code: "base-url-missing",
|
|
776
|
+
message: "Provider base URL is missing",
|
|
777
|
+
};
|
|
778
|
+
checks.push({ name: "base-url", status: "fail", ...next });
|
|
779
|
+
recordFailure(next);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
url = sanitizeUrl(target.endpoint.baseUrl);
|
|
783
|
+
if (!url) {
|
|
784
|
+
const next = {
|
|
785
|
+
category: "config",
|
|
786
|
+
code: "base-url-invalid",
|
|
787
|
+
message: "Provider base URL is invalid",
|
|
788
|
+
};
|
|
789
|
+
checks.push({ name: "base-url", status: "fail", ...next });
|
|
790
|
+
recordFailure(next);
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
const allowedScheme = target.origin === "local-proxy"
|
|
794
|
+
? url.protocol === "http:" || url.protocol === "https:"
|
|
795
|
+
: url.protocol === "https:";
|
|
796
|
+
if (!allowedScheme) {
|
|
797
|
+
const next = {
|
|
798
|
+
category: "config",
|
|
799
|
+
code: "url-scheme-unsupported",
|
|
800
|
+
message: "Provider URL scheme is not allowed",
|
|
801
|
+
};
|
|
802
|
+
checks.push({ name: "base-url", status: "fail", ...next });
|
|
803
|
+
recordFailure(next);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
checks.push({ name: "base-url", status: "ok", code: "ok", message: "Provider base URL is valid" });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (target.origin === "unknown") {
|
|
811
|
+
const next = {
|
|
812
|
+
category: "config",
|
|
813
|
+
code: "origin-unsupported",
|
|
814
|
+
message: "Provider origin is unsupported",
|
|
815
|
+
};
|
|
816
|
+
checks.push({ name: "origin-policy", status: "fail", ...next });
|
|
817
|
+
recordFailure(next);
|
|
818
|
+
}
|
|
819
|
+
else if (url) {
|
|
820
|
+
const hostname = normalizedHostname(url);
|
|
821
|
+
const blocked = target.origin === "local-proxy" ? !isLiteralLoopback(hostname) : isBlockedPublicHost(hostname);
|
|
822
|
+
if (blocked) {
|
|
823
|
+
const next = {
|
|
824
|
+
category: "config",
|
|
825
|
+
code: "address-policy-blocked",
|
|
826
|
+
message: "Provider address is blocked by origin policy",
|
|
827
|
+
};
|
|
828
|
+
checks.push({ name: "origin-policy", status: "fail", ...next });
|
|
829
|
+
recordFailure(next);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
checks.push({ name: "origin-policy", status: "ok", code: "ok", message: "Provider origin policy passed" });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
checks.push({ name: "origin-policy", status: "skipped", code: "ok", message: "No valid URL to inspect" });
|
|
837
|
+
}
|
|
838
|
+
if (target.auth.present) {
|
|
839
|
+
checks.push({ name: "auth-present", status: "ok", code: "ok", message: "Authentication is configured" });
|
|
840
|
+
}
|
|
841
|
+
else if (target.origin === "custom-openai-compatible" || target.origin === "local-proxy") {
|
|
842
|
+
// Native login checks do not apply: custom/local OpenAI-compatible endpoints may be keyless.
|
|
843
|
+
checks.push({
|
|
844
|
+
name: "auth-present",
|
|
845
|
+
status: "skipped",
|
|
846
|
+
code: "ok",
|
|
847
|
+
message: "No credential configured; custom endpoints may be keyless",
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
const next = {
|
|
852
|
+
category: "auth",
|
|
853
|
+
code: "auth-missing",
|
|
854
|
+
message: "Authentication is not configured",
|
|
30
855
|
};
|
|
856
|
+
checks.push({ name: "auth-present", status: "fail", ...next });
|
|
857
|
+
recordFailure(next);
|
|
858
|
+
}
|
|
859
|
+
return { checks, url, failure };
|
|
860
|
+
}
|
|
861
|
+
function classifyProbe(name, kind, status) {
|
|
862
|
+
const forModels = kind === "models";
|
|
863
|
+
if (Number.isInteger(status) && status >= 200 && status < 300) {
|
|
864
|
+
const probe = { reachable: true, authenticated: true, status, category: "ok" };
|
|
865
|
+
if (forModels)
|
|
866
|
+
probe.modelsSupported = true;
|
|
867
|
+
return { check: { name, status: "ok", code: "ok", message: `HTTP ${status}`, probe } };
|
|
31
868
|
}
|
|
32
|
-
|
|
33
|
-
|
|
869
|
+
if (status >= 300 && status < 400) {
|
|
870
|
+
const failure = {
|
|
871
|
+
category: "config",
|
|
872
|
+
code: "redirect-blocked",
|
|
873
|
+
message: "Provider probe returned a blocked redirect",
|
|
874
|
+
};
|
|
875
|
+
return { check: { name, status: "fail", ...failure }, failure };
|
|
876
|
+
}
|
|
877
|
+
if (status === 401 || status === 403) {
|
|
878
|
+
const failure = {
|
|
879
|
+
category: "auth",
|
|
880
|
+
code: "authentication-failed",
|
|
881
|
+
message: "Provider rejected authentication",
|
|
882
|
+
};
|
|
883
|
+
const probe = { reachable: true, authenticated: false, status, category: "auth" };
|
|
884
|
+
return { check: { name, status: "fail", ...failure, probe }, failure };
|
|
885
|
+
}
|
|
886
|
+
if (status === 404 || status === 405 || status === 501) {
|
|
887
|
+
// A root or /models 404 is neutral: OpenAI-compatible servers may not expose these paths.
|
|
888
|
+
const probe = { reachable: true, status, category: "unsupported-endpoint" };
|
|
889
|
+
if (forModels)
|
|
890
|
+
probe.modelsSupported = false;
|
|
34
891
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
892
|
+
check: {
|
|
893
|
+
name,
|
|
894
|
+
status: "unsupported",
|
|
895
|
+
code: "endpoint-unsupported",
|
|
896
|
+
message: `HTTP ${status}; endpoint is not required`,
|
|
897
|
+
probe,
|
|
39
898
|
},
|
|
40
|
-
source: "built-in-grok-oauth-proxy-defaults",
|
|
41
899
|
};
|
|
42
900
|
}
|
|
43
|
-
|
|
901
|
+
if (status >= 500 && status <= 599) {
|
|
902
|
+
const failure = {
|
|
903
|
+
category: "server",
|
|
904
|
+
code: "server-error",
|
|
905
|
+
message: "Provider endpoint returned a server error",
|
|
906
|
+
};
|
|
907
|
+
const probe = { reachable: true, status, category: "server" };
|
|
908
|
+
return { check: { name, status: "fail", ...failure, probe }, failure };
|
|
909
|
+
}
|
|
910
|
+
if (status === 408 || status === 429) {
|
|
911
|
+
const failure = {
|
|
912
|
+
category: "network",
|
|
913
|
+
code: "network-failure",
|
|
914
|
+
message: "Provider probe failed at the network boundary",
|
|
915
|
+
};
|
|
916
|
+
const probe = { reachable: true, status, category: "network" };
|
|
917
|
+
return { check: { name, status: "fail", ...failure, probe }, failure };
|
|
918
|
+
}
|
|
919
|
+
const failure = {
|
|
920
|
+
category: "config",
|
|
921
|
+
code: "unexpected-response",
|
|
922
|
+
message: "Provider probe returned an unexpected response",
|
|
923
|
+
};
|
|
924
|
+
return { check: { name, status: "fail", ...failure }, failure };
|
|
44
925
|
}
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
926
|
+
function createComposedController(signal, timeoutMs) {
|
|
927
|
+
const controller = new AbortController();
|
|
928
|
+
let kind;
|
|
929
|
+
const abortFromCaller = () => {
|
|
930
|
+
kind ??= "aborted";
|
|
931
|
+
controller.abort();
|
|
932
|
+
};
|
|
933
|
+
if (signal?.aborted)
|
|
934
|
+
abortFromCaller();
|
|
935
|
+
else
|
|
936
|
+
signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
937
|
+
const timeout = setTimeout(() => {
|
|
938
|
+
kind ??= "timeout";
|
|
939
|
+
controller.abort();
|
|
940
|
+
}, timeoutMs);
|
|
941
|
+
return {
|
|
942
|
+
controller,
|
|
943
|
+
abortKind: () => kind,
|
|
944
|
+
dispose: () => {
|
|
945
|
+
clearTimeout(timeout);
|
|
946
|
+
signal?.removeEventListener("abort", abortFromCaller);
|
|
947
|
+
},
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function requestWithAbort(transport, request) {
|
|
951
|
+
return new Promise((resolve, reject) => {
|
|
952
|
+
if (request.signal.aborted) {
|
|
953
|
+
reject(ABORTED);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const onAbort = () => reject(ABORTED);
|
|
957
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
958
|
+
let pending;
|
|
66
959
|
try {
|
|
67
|
-
|
|
68
|
-
const res = await fetch(probeUrl, { method: "GET" });
|
|
69
|
-
checks.push({
|
|
70
|
-
name: "base-url-reachable",
|
|
71
|
-
status: res.ok ? "ok" : "fail",
|
|
72
|
-
message: `${probeUrl} → HTTP ${res.status}`,
|
|
73
|
-
});
|
|
960
|
+
pending = transport.request(request);
|
|
74
961
|
}
|
|
75
|
-
catch (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
message: err instanceof Error ? err.message : String(err),
|
|
80
|
-
});
|
|
962
|
+
catch (error) {
|
|
963
|
+
request.signal.removeEventListener("abort", onAbort);
|
|
964
|
+
reject(error instanceof ProviderDoctorAuthMaterializationError ? error : undefined);
|
|
965
|
+
return;
|
|
81
966
|
}
|
|
967
|
+
void pending.then((response) => {
|
|
968
|
+
request.signal.removeEventListener("abort", onAbort);
|
|
969
|
+
resolve(response);
|
|
970
|
+
}, (error) => {
|
|
971
|
+
request.signal.removeEventListener("abort", onAbort);
|
|
972
|
+
reject(error instanceof ProviderDoctorAuthMaterializationError ? error : undefined);
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
async function runProbe(name, kind, url, transport, addressPolicy, createHeaders, controller, payload) {
|
|
977
|
+
try {
|
|
978
|
+
const response = await requestWithAbort(transport, {
|
|
979
|
+
url,
|
|
980
|
+
method: payload ? "POST" : "GET",
|
|
981
|
+
redirect: "manual",
|
|
982
|
+
signal: controller.controller.signal,
|
|
983
|
+
addressPolicy,
|
|
984
|
+
createHeaders,
|
|
985
|
+
body: payload?.body,
|
|
986
|
+
contentType: payload?.contentType,
|
|
987
|
+
});
|
|
988
|
+
return classifyProbe(name, kind, response.status);
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
if (error instanceof ProviderDoctorAuthMaterializationError) {
|
|
992
|
+
const failure = {
|
|
993
|
+
category: "auth",
|
|
994
|
+
code: "auth-materialization-failed",
|
|
995
|
+
message: "Configured authentication could not be materialized; verify auth and config references",
|
|
996
|
+
};
|
|
997
|
+
return {
|
|
998
|
+
check: {
|
|
999
|
+
name,
|
|
1000
|
+
status: "fail",
|
|
1001
|
+
...failure,
|
|
1002
|
+
probe: { reachable: false, authenticated: false, category: "auth" },
|
|
1003
|
+
},
|
|
1004
|
+
failure,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const abortKind = controller.abortKind();
|
|
1008
|
+
const failure = abortKind
|
|
1009
|
+
? {
|
|
1010
|
+
category: "network",
|
|
1011
|
+
code: abortKind === "timeout" ? "request-timeout" : "request-aborted",
|
|
1012
|
+
message: abortKind === "timeout" ? "Provider probe timed out" : "Provider probe was aborted",
|
|
1013
|
+
}
|
|
1014
|
+
: { category: "network", code: "network-failure", message: "Provider transport failed" };
|
|
1015
|
+
return {
|
|
1016
|
+
check: { name, status: "fail", ...failure, probe: { reachable: false, category: "network" } },
|
|
1017
|
+
failure,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/** Minimal-token generative probe payload; never includes tools (no tool-call probe). */
|
|
1022
|
+
function modelProbeSpec(api, modelId) {
|
|
1023
|
+
if (api === "openai-completions") {
|
|
1024
|
+
return {
|
|
1025
|
+
path: "chat/completions",
|
|
1026
|
+
payload: {
|
|
1027
|
+
body: JSON.stringify({
|
|
1028
|
+
model: modelId,
|
|
1029
|
+
messages: [{ role: "user", content: "ping" }],
|
|
1030
|
+
max_tokens: 1,
|
|
1031
|
+
stream: false,
|
|
1032
|
+
}),
|
|
1033
|
+
contentType: "application/json",
|
|
1034
|
+
},
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
if (api === "openai-responses") {
|
|
1038
|
+
return {
|
|
1039
|
+
path: "responses",
|
|
1040
|
+
payload: {
|
|
1041
|
+
// The responses API rejects max_output_tokens below 16; still the minimal request.
|
|
1042
|
+
body: JSON.stringify({ model: modelId, input: "ping", max_output_tokens: 16, stream: false }),
|
|
1043
|
+
contentType: "application/json",
|
|
1044
|
+
},
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
function classifyOrigin(providerId, baseUrl, api, isNative) {
|
|
1050
|
+
const url = baseUrl ? sanitizeUrl(baseUrl) : undefined;
|
|
1051
|
+
if (providerId === "grok-oauth-proxy" || (url && isLiteralLoopback(normalizedHostname(url)))) {
|
|
1052
|
+
return "local-proxy";
|
|
1053
|
+
}
|
|
1054
|
+
if (isNative)
|
|
1055
|
+
return "native";
|
|
1056
|
+
if (api && OPENAI_COMPATIBLE_APIS.has(api))
|
|
1057
|
+
return "custom-openai-compatible";
|
|
1058
|
+
return "unknown";
|
|
1059
|
+
}
|
|
1060
|
+
function resolveProviderTarget(providerId, options) {
|
|
1061
|
+
const level = options.level ?? 0;
|
|
1062
|
+
const agentDir = options.agentDir ?? getConfiguredAgentDir();
|
|
1063
|
+
const modelsConfig = loadConfig(join(agentDir, "models.json"), parseModelsConfig, true);
|
|
1064
|
+
if (modelsConfig.kind === "invalid") {
|
|
1065
|
+
return {
|
|
1066
|
+
failure: unresolvedFailure(providerId, level, "models-config-invalid", "models.json is invalid"),
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
const authConfig = loadConfig(join(agentDir, "auth.json"), parseAuthConfig);
|
|
1070
|
+
if (authConfig.kind === "invalid") {
|
|
1071
|
+
return { failure: unresolvedFailure(providerId, level, "auth-config-invalid", "auth.json is invalid") };
|
|
1072
|
+
}
|
|
1073
|
+
const kimi = providerId === KIMI_PROVIDER_ID ? resolveKimiConfig(options.kimiConfigPath) : {};
|
|
1074
|
+
if (kimi.failure) {
|
|
1075
|
+
return {
|
|
1076
|
+
failure: unresolvedFailure(providerId, level, "kimi-config-invalid", `Kimi config TOML is invalid (line ${kimi.failure.line}: ${kimi.failure.reason}); fix the Kimi config or set KIMI_BASE_URL/KIMI_MODEL_NAME`),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const configured = modelsConfig.kind === "ok" ? modelsConfig.value.providers[providerId] : undefined;
|
|
1080
|
+
const nativeProviders = getProviders();
|
|
1081
|
+
const isNative = nativeProviders.includes(providerId);
|
|
1082
|
+
const nativeModels = isNative ? getModels(providerId) : [];
|
|
1083
|
+
const configuredModels = configured?.models ?? [];
|
|
1084
|
+
// At Level 2 the probe model is the paid target and therefore drives endpoint resolution.
|
|
1085
|
+
const requestedModel = level === 2 ? (options.probeModelId ?? options.modelId) : options.modelId;
|
|
1086
|
+
const configuredModel = requestedModel
|
|
1087
|
+
? configuredModels.find((model) => model.id === requestedModel)
|
|
1088
|
+
: configuredModels[0];
|
|
1089
|
+
// §10.2: with no higher-precedence endpoint (models.json > KIMI_BASE_URL > root TOML), a
|
|
1090
|
+
// `[providers.<name>]` entry with `type = "openai_legacy"` selects a custom OpenAI-compatible
|
|
1091
|
+
// endpoint, so native-only checks and native credential/model inheritance are skipped downstream.
|
|
1092
|
+
const kimiCustomSelected = !(configuredModel?.baseUrl ?? configured?.baseUrl) && !kimi.baseUrl && kimi.customProvider !== undefined;
|
|
1093
|
+
const kimiModelName = kimi.modelName ?? (kimiCustomSelected ? kimi.customProvider?.modelName : undefined);
|
|
1094
|
+
const endpointModelId = requestedModel ?? kimiModelName;
|
|
1095
|
+
const modelIds = kimiCustomSelected
|
|
1096
|
+
? kimiModelName
|
|
1097
|
+
? [kimiModelName]
|
|
1098
|
+
: []
|
|
1099
|
+
: Array.from(new Set([
|
|
1100
|
+
...nativeModels.map((model) => model.id),
|
|
1101
|
+
...configuredModels.map((model) => model.id),
|
|
1102
|
+
...(kimiModelName ? [kimiModelName] : []),
|
|
1103
|
+
]));
|
|
1104
|
+
let nativeModel = kimiCustomSelected
|
|
1105
|
+
? undefined
|
|
1106
|
+
: requestedModel
|
|
1107
|
+
? nativeModels.find((model) => model.id === requestedModel)
|
|
1108
|
+
: nativeModels[0];
|
|
1109
|
+
if (!nativeModel && !kimiCustomSelected && requestedModel !== undefined && requestedModel === kimiModelName) {
|
|
1110
|
+
// A native Kimi TOML/env model rides on the provider defaults for endpoint metadata.
|
|
1111
|
+
nativeModel = nativeModels[0];
|
|
1112
|
+
}
|
|
1113
|
+
let baseUrl = configuredModel?.baseUrl ?? configured?.baseUrl;
|
|
1114
|
+
let api = configuredModel?.api ?? configured?.api ?? nativeModel?.api;
|
|
1115
|
+
let source = configured ? "models.json" : isNative ? "built-in-model-registry" : "unresolved";
|
|
1116
|
+
if (!baseUrl && kimi.baseUrl) {
|
|
1117
|
+
baseUrl = kimi.baseUrl;
|
|
1118
|
+
source = kimi.baseUrlSource ?? source;
|
|
1119
|
+
}
|
|
1120
|
+
else if (kimiCustomSelected && kimi.customProvider) {
|
|
1121
|
+
baseUrl = kimi.customProvider.baseUrl;
|
|
1122
|
+
api = "openai-completions";
|
|
1123
|
+
source = `kimi-config-toml:providers.${kimi.customProvider.name}`;
|
|
82
1124
|
}
|
|
83
1125
|
else {
|
|
84
|
-
|
|
1126
|
+
baseUrl = baseUrl ?? nativeModel?.baseUrl;
|
|
85
1127
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1128
|
+
let builtInAuth = false;
|
|
1129
|
+
if (providerId === "grok-oauth-proxy" && !baseUrl) {
|
|
1130
|
+
baseUrl = "http://127.0.0.1:9996/v1";
|
|
1131
|
+
api = "openai-completions";
|
|
1132
|
+
source = "built-in-grok-oauth-proxy-defaults";
|
|
1133
|
+
builtInAuth = true;
|
|
1134
|
+
}
|
|
1135
|
+
const storedAuth = authConfig.kind === "ok" && authConfig.value[providerId] !== undefined;
|
|
1136
|
+
const environmentAuth = findEnvKeys(providerId) !== undefined;
|
|
1137
|
+
const configuredAuth = configured?.apiKey ? isConfigValueConfigured(configured.apiKey) : false;
|
|
1138
|
+
const headerAuth = configured?.headers !== undefined && Object.keys(configured.headers).length > 0;
|
|
1139
|
+
const customApiKey = kimiCustomSelected ? kimi.customProvider?.apiKey : undefined;
|
|
1140
|
+
const customAuthPresent = customApiKey !== undefined && isConfigValueConfigured(customApiKey);
|
|
1141
|
+
const authPresent = kimiCustomSelected
|
|
1142
|
+
? customAuthPresent
|
|
1143
|
+
: storedAuth || environmentAuth || configuredAuth || headerAuth || builtInAuth;
|
|
1144
|
+
const authSource = kimiCustomSelected
|
|
1145
|
+
? customAuthPresent
|
|
1146
|
+
? source
|
|
1147
|
+
: undefined
|
|
1148
|
+
: storedAuth
|
|
1149
|
+
? "auth.json"
|
|
1150
|
+
: environmentAuth
|
|
1151
|
+
? "environment"
|
|
1152
|
+
: configuredAuth || headerAuth
|
|
1153
|
+
? "models.json"
|
|
1154
|
+
: builtInAuth
|
|
1155
|
+
? "built-in"
|
|
1156
|
+
: undefined;
|
|
1157
|
+
if (!configured && !isNative && !storedAuth && !environmentAuth && providerId !== "grok-oauth-proxy") {
|
|
1158
|
+
return { failure: unresolvedFailure(providerId, level, "provider-not-found", "Provider was not found") };
|
|
1159
|
+
}
|
|
1160
|
+
const origin = classifyOrigin(providerId, baseUrl, api, isNative && !kimiCustomSelected);
|
|
1161
|
+
const canonicalBaseUrl = canonicalProviderEndpointUrl(baseUrl);
|
|
1162
|
+
const binding = canonicalBaseUrl && api && endpointModelId
|
|
1163
|
+
? { origin, source, baseUrl: canonicalBaseUrl, api, modelId: endpointModelId }
|
|
1164
|
+
: undefined;
|
|
1165
|
+
const createHeaders = kimiCustomSelected && binding
|
|
1166
|
+
? () => {
|
|
1167
|
+
const headers = new Headers();
|
|
1168
|
+
if (customApiKey) {
|
|
1169
|
+
headers.set("Authorization", `Bearer ${resolveConfigValueOrThrow(customApiKey, "API key for custom Kimi endpoint")}`);
|
|
1170
|
+
}
|
|
1171
|
+
return headers;
|
|
104
1172
|
}
|
|
105
|
-
|
|
106
|
-
|
|
1173
|
+
: undefined;
|
|
1174
|
+
return {
|
|
1175
|
+
target: {
|
|
1176
|
+
providerId,
|
|
1177
|
+
origin,
|
|
1178
|
+
source,
|
|
1179
|
+
endpoint: { baseUrl, api, modelIds, modelId: endpointModelId },
|
|
1180
|
+
auth: { present: authPresent, source: authSource, createHeaders, binding },
|
|
1181
|
+
},
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
export async function diagnoseResolvedProvider(target, options = {}, dependencies = {}) {
|
|
1185
|
+
const requestedLevel = options.level ?? 0;
|
|
1186
|
+
if (requestedLevel !== 0 && requestedLevel !== 1 && requestedLevel !== 2) {
|
|
1187
|
+
return unresolvedFailure(target.providerId, 0, "unsupported-level", "Provider doctor level is unsupported");
|
|
1188
|
+
}
|
|
1189
|
+
const level = requestedLevel;
|
|
1190
|
+
// Level 2 is opt-in only: probeModelId is ignored at levels 0/1, which stay GET-only.
|
|
1191
|
+
const probeModelId = level === 2 ? options.probeModelId : undefined;
|
|
1192
|
+
const effectiveModelId = probeModelId ?? options.modelId;
|
|
1193
|
+
const resolvedTarget = effectiveModelId
|
|
1194
|
+
? { ...target, endpoint: { ...target.endpoint, modelId: effectiveModelId } }
|
|
1195
|
+
: target;
|
|
1196
|
+
if (level === 2 && options.modelId && probeModelId && options.modelId !== probeModelId) {
|
|
1197
|
+
const failure = {
|
|
1198
|
+
category: "model",
|
|
1199
|
+
code: "probe-model-conflict",
|
|
1200
|
+
message: "Validation model and paid probe model must match",
|
|
1201
|
+
};
|
|
1202
|
+
return publicResult(resolvedTarget, level, [{ name: "model-probe", status: "fail", ...failure }], undefined, failure);
|
|
1203
|
+
}
|
|
1204
|
+
if (level === 2 && !probeModelId) {
|
|
1205
|
+
const failure = {
|
|
1206
|
+
category: "config",
|
|
1207
|
+
code: "probe-model-required",
|
|
1208
|
+
message: "Level 2 requires an explicit probe model (--probe-model)",
|
|
1209
|
+
};
|
|
1210
|
+
return publicResult(resolvedTarget, level, [{ name: "model-probe", status: "fail", ...failure }], undefined, failure);
|
|
1211
|
+
}
|
|
1212
|
+
const validation = staticChecks(resolvedTarget);
|
|
1213
|
+
if (validation.failure || level === 0) {
|
|
1214
|
+
return publicResult(resolvedTarget, level, validation.checks, validation.url, validation.failure);
|
|
1215
|
+
}
|
|
1216
|
+
const transport = dependencies.transport ?? options.transport;
|
|
1217
|
+
if (!transport) {
|
|
1218
|
+
const failure = {
|
|
1219
|
+
category: "config",
|
|
1220
|
+
code: "transport-required",
|
|
1221
|
+
message: "Level 1 requires an injected provider transport",
|
|
1222
|
+
};
|
|
1223
|
+
validation.checks.push({ name: "transport", status: "fail", ...failure });
|
|
1224
|
+
return publicResult(resolvedTarget, level, validation.checks, validation.url, failure);
|
|
1225
|
+
}
|
|
1226
|
+
if (resolvedTarget.origin !== "local-proxy" && transport.pinsResolvedAddress !== true) {
|
|
1227
|
+
const failure = {
|
|
1228
|
+
category: "config",
|
|
1229
|
+
code: "address-pinning-required",
|
|
1230
|
+
message: "Public provider transport must pin resolved addresses",
|
|
1231
|
+
};
|
|
1232
|
+
validation.checks.push({ name: "transport", status: "fail", ...failure });
|
|
1233
|
+
return publicResult(resolvedTarget, level, validation.checks, validation.url, failure);
|
|
1234
|
+
}
|
|
1235
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1236
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1237
|
+
const failure = {
|
|
1238
|
+
category: "config",
|
|
1239
|
+
code: "unexpected-response",
|
|
1240
|
+
message: "Provider probe timeout is invalid",
|
|
1241
|
+
};
|
|
1242
|
+
validation.checks.push({ name: "transport", status: "fail", ...failure });
|
|
1243
|
+
return publicResult(resolvedTarget, level, validation.checks, validation.url, failure);
|
|
1244
|
+
}
|
|
1245
|
+
const addressPolicy = resolvedTarget.origin === "local-proxy"
|
|
1246
|
+
? { kind: "loopback-only", requireAddressPinning: false }
|
|
1247
|
+
: { kind: "public", requireAddressPinning: true };
|
|
1248
|
+
const resolveAuthHeaders = dependencies.resolveAuthHeaders ?? options.resolveAuthHeaders;
|
|
1249
|
+
const credentialBinding = resolvedTarget.auth.binding;
|
|
1250
|
+
const boundHeaderFactory = resolvedTarget.auth.createHeaders;
|
|
1251
|
+
const endpointBoundHeaders = credentialBinding && boundHeaderFactory
|
|
1252
|
+
? async () => {
|
|
1253
|
+
if (!credentialBindingMatchesTarget(credentialBinding, resolvedTarget)) {
|
|
1254
|
+
throw new ProviderDoctorAuthMaterializationError();
|
|
1255
|
+
}
|
|
1256
|
+
try {
|
|
1257
|
+
return await boundHeaderFactory();
|
|
1258
|
+
}
|
|
1259
|
+
catch {
|
|
1260
|
+
throw new ProviderDoctorAuthMaterializationError();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
: undefined;
|
|
1264
|
+
const createHeaders = endpointBoundHeaders
|
|
1265
|
+
? endpointBoundHeaders
|
|
1266
|
+
: level === 2 && resolveAuthHeaders
|
|
1267
|
+
? async () => {
|
|
1268
|
+
if (credentialBinding && !credentialBindingMatchesTarget(credentialBinding, resolvedTarget)) {
|
|
1269
|
+
throw new ProviderDoctorAuthMaterializationError();
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
return await resolveAuthHeaders(resolvedTarget);
|
|
1273
|
+
}
|
|
1274
|
+
catch {
|
|
1275
|
+
throw new ProviderDoctorAuthMaterializationError();
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
: (resolvedTarget.auth.createHeaders ?? (() => new Headers()));
|
|
1279
|
+
const controller = createComposedController(options.signal, timeoutMs);
|
|
1280
|
+
try {
|
|
1281
|
+
const root = await runProbe("root-endpoint", "root", makeRootProbeUrl(validation.url, resolvedTarget.origin), transport, addressPolicy, createHeaders, controller);
|
|
1282
|
+
validation.checks.push(root.check);
|
|
1283
|
+
if (root.failure) {
|
|
1284
|
+
validation.checks.push({
|
|
107
1285
|
name: "models-endpoint",
|
|
108
|
-
status: "
|
|
109
|
-
|
|
1286
|
+
status: "skipped",
|
|
1287
|
+
code: "ok",
|
|
1288
|
+
message: "Capability probe skipped after root failure",
|
|
110
1289
|
});
|
|
1290
|
+
return publicResult(resolvedTarget, level, validation.checks, validation.url, root.failure);
|
|
1291
|
+
}
|
|
1292
|
+
const models = await runProbe("models-endpoint", "models", appendPath(validation.url, "models"), transport, addressPolicy, createHeaders, controller);
|
|
1293
|
+
validation.checks.push(models.check);
|
|
1294
|
+
if (level !== 2 || models.failure) {
|
|
1295
|
+
return publicResult(resolvedTarget, level, validation.checks, validation.url, models.failure);
|
|
111
1296
|
}
|
|
1297
|
+
const spec = modelProbeSpec(resolvedTarget.endpoint.api ?? "", probeModelId);
|
|
1298
|
+
if (!spec) {
|
|
1299
|
+
const failure = {
|
|
1300
|
+
category: "config",
|
|
1301
|
+
code: "api-unsupported",
|
|
1302
|
+
message: "Model probe supports only OpenAI-compatible APIs",
|
|
1303
|
+
};
|
|
1304
|
+
validation.checks.push({ name: "model-probe", status: "fail", ...failure });
|
|
1305
|
+
return publicResult(resolvedTarget, level, validation.checks, validation.url, failure);
|
|
1306
|
+
}
|
|
1307
|
+
const probe = await runProbe("model-probe", "model-probe", appendPath(validation.url, spec.path), transport, addressPolicy, createHeaders, controller, spec.payload);
|
|
1308
|
+
const dispatched = probe.failure?.code !== "auth-materialization-failed";
|
|
1309
|
+
if (dispatched) {
|
|
1310
|
+
probe.check.message = `${probe.check.message}; minimal-token generative probe (may incur provider costs)`;
|
|
1311
|
+
}
|
|
1312
|
+
validation.checks.push(probe.check);
|
|
1313
|
+
return publicResult(resolvedTarget, level, validation.checks, validation.url, probe.failure, dispatched);
|
|
112
1314
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const failed = checks.find((c) => c.status === "fail");
|
|
117
|
-
let error;
|
|
118
|
-
if (failed) {
|
|
119
|
-
const category = failed.name === "base-url-reachable"
|
|
120
|
-
? "network"
|
|
121
|
-
: failed.name === "api-key-present"
|
|
122
|
-
? "auth"
|
|
123
|
-
: failed.name === "models-endpoint"
|
|
124
|
-
? "model"
|
|
125
|
-
: "unknown";
|
|
126
|
-
error = { category, message: failed.message ?? `${failed.name} failed` };
|
|
1315
|
+
finally {
|
|
1316
|
+
controller.dispose();
|
|
127
1317
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1318
|
+
}
|
|
1319
|
+
export async function diagnoseProvider(providerId, options = {}) {
|
|
1320
|
+
const requestedLevel = options.level ?? 0;
|
|
1321
|
+
if (requestedLevel !== 0 && requestedLevel !== 1 && requestedLevel !== 2) {
|
|
1322
|
+
return unresolvedFailure(providerId, 0, "unsupported-level", "Provider doctor level is unsupported");
|
|
1323
|
+
}
|
|
1324
|
+
const resolved = resolveProviderTarget(providerId, options);
|
|
1325
|
+
if (resolved.failure)
|
|
1326
|
+
return resolved.failure;
|
|
1327
|
+
return diagnoseResolvedProvider(resolved.target, options);
|
|
135
1328
|
}
|
|
136
1329
|
//# sourceMappingURL=doctor-provider.js.map
|