open-multi-agent-kit 1.2.2 → 1.2.4

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.
Files changed (84) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +2 -2
  3. package/dist/cli/help.d.ts.map +1 -1
  4. package/dist/cli/help.js +2 -0
  5. package/dist/cli/help.js.map +1 -1
  6. package/dist/commands/doctor-provider.d.ts.map +1 -1
  7. package/dist/commands/doctor-provider.js +2 -13
  8. package/dist/commands/doctor-provider.js.map +1 -1
  9. package/dist/commands/provider-adopt-cli.d.ts +24 -0
  10. package/dist/commands/provider-adopt-cli.d.ts.map +1 -0
  11. package/dist/commands/provider-adopt-cli.js +182 -0
  12. package/dist/commands/provider-adopt-cli.js.map +1 -0
  13. package/dist/commands/provider-adopt-status.d.ts +21 -0
  14. package/dist/commands/provider-adopt-status.d.ts.map +1 -0
  15. package/dist/commands/provider-adopt-status.js +84 -0
  16. package/dist/commands/provider-adopt-status.js.map +1 -0
  17. package/dist/commands/provider-known-apis.d.ts +2 -0
  18. package/dist/commands/provider-known-apis.d.ts.map +1 -0
  19. package/dist/commands/provider-known-apis.js +28 -0
  20. package/dist/commands/provider-known-apis.js.map +1 -0
  21. package/dist/commands/run-command.d.ts.map +1 -1
  22. package/dist/commands/run-command.js +4 -0
  23. package/dist/commands/run-command.js.map +1 -1
  24. package/dist/core/agent-session.d.ts.map +1 -1
  25. package/dist/core/agent-session.js +6 -0
  26. package/dist/core/agent-session.js.map +1 -1
  27. package/dist/core/auth-storage.d.ts +16 -16
  28. package/dist/core/auth-storage.d.ts.map +1 -1
  29. package/dist/core/auth-storage.js +94 -111
  30. package/dist/core/auth-storage.js.map +1 -1
  31. package/dist/core/compaction/compaction.d.ts.map +1 -1
  32. package/dist/core/compaction/compaction.js +13 -10
  33. package/dist/core/compaction/compaction.js.map +1 -1
  34. package/dist/core/compaction/inert-tail.d.ts +23 -0
  35. package/dist/core/compaction/inert-tail.d.ts.map +1 -0
  36. package/dist/core/compaction/inert-tail.js +108 -0
  37. package/dist/core/compaction/inert-tail.js.map +1 -0
  38. package/dist/core/compaction/provenance.d.ts +30 -0
  39. package/dist/core/compaction/provenance.d.ts.map +1 -0
  40. package/dist/core/compaction/provenance.js +59 -0
  41. package/dist/core/compaction/provenance.js.map +1 -0
  42. package/dist/core/compaction/transaction.d.ts.map +1 -1
  43. package/dist/core/compaction/transaction.js +4 -1
  44. package/dist/core/compaction/transaction.js.map +1 -1
  45. package/dist/core/external-credential-sources.d.ts +48 -0
  46. package/dist/core/external-credential-sources.d.ts.map +1 -0
  47. package/dist/core/external-credential-sources.js +176 -0
  48. package/dist/core/external-credential-sources.js.map +1 -0
  49. package/dist/core/model-registry.d.ts +11 -0
  50. package/dist/core/model-registry.d.ts.map +1 -1
  51. package/dist/core/model-registry.js +28 -14
  52. package/dist/core/model-registry.js.map +1 -1
  53. package/dist/core/oauth-account-import.d.ts +52 -0
  54. package/dist/core/oauth-account-import.d.ts.map +1 -0
  55. package/dist/core/oauth-account-import.js +125 -0
  56. package/dist/core/oauth-account-import.js.map +1 -0
  57. package/dist/core/oauth-credential.d.ts +22 -0
  58. package/dist/core/oauth-credential.d.ts.map +1 -0
  59. package/dist/core/oauth-credential.js +36 -0
  60. package/dist/core/oauth-credential.js.map +1 -0
  61. package/dist/core/session-compaction-service.d.ts +2 -2
  62. package/dist/core/session-compaction-service.d.ts.map +1 -1
  63. package/dist/core/session-compaction-service.js +20 -57
  64. package/dist/core/session-compaction-service.js.map +1 -1
  65. package/dist/core/verified-run/owned-execution.d.ts.map +1 -1
  66. package/dist/core/verified-run/owned-execution.js +13 -1
  67. package/dist/core/verified-run/owned-execution.js.map +1 -1
  68. package/dist/core/verified-run/supervisor-adapter.d.ts +4 -0
  69. package/dist/core/verified-run/supervisor-adapter.d.ts.map +1 -1
  70. package/dist/core/verified-run/supervisor-adapter.js +4 -0
  71. package/dist/core/verified-run/supervisor-adapter.js.map +1 -1
  72. package/docs/compaction.md +2 -0
  73. package/docs/extensions.md +1 -1
  74. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  75. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  76. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  77. package/examples/extensions/gondolin/package-lock.json +2 -2
  78. package/examples/extensions/gondolin/package.json +1 -1
  79. package/examples/extensions/sandbox/package-lock.json +2 -2
  80. package/examples/extensions/sandbox/package.json +1 -1
  81. package/examples/extensions/with-deps/package-lock.json +2 -2
  82. package/examples/extensions/with-deps/package.json +1 -1
  83. package/npm-shrinkwrap.json +18 -18
  84. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [1.2.4] - 2026-09-23
6
+
7
+ ### New Features
8
+
9
+ - `omk provider adopt [<id>] [--from <source>] [--dry-run] [--json] [--status]` copies an existing Codex CLI or Claude Code CLI login into OMK's credential store, so a subscription does not have to be signed in twice. Sources are read-only, `--from` narrows to the provider's own mapping, and no token material reaches output.
10
+
11
+ ### Fixed
12
+
13
+ - A credential store that cannot be read is no longer treated as "no credential": request auth refuses to substitute stale environment or models.json keys, `agent-session` reports the store error instead of advising `/login`, and a transient lock contention is retried instead of deciding authentication for the whole process lifetime.
14
+ - `omk provider doctor` accepts engine-registered API types (`devin-agent`, `cursor-agent`) instead of rejecting them as unsupported.
15
+ - Compaction commits survive append-only extension state written while the summary is generated; only provably inert `custom` tails are rebased, and a `length` stop that produced no summary text fails instead of committing an empty summary. The compaction source-entry bound is 65,536.
16
+ - `addOAuthAccount` merges imported accounts inside the storage lock (no lost update across concurrent sessions) and never overwrites a usable stored refresh token with an absent one.
17
+ - A post-dispatch rejection that provably never spawned (a deadline crossing between the dispatch journal and the supervisor call) now journals the exit and settles the run instead of leaving the execution id open forever.
18
+
19
+ ## [1.2.3] - 2026-09-23
20
+
21
+ ### Fixed
22
+
23
+ - Verified-run suite hardening: tests use a 15s cleanup budget so slow namespace teardown on ubuntu-22.04 runners drains before settling, matching the witness contract without weakening fail-closed outcomes (no product-code change).
24
+
3
25
  ## [1.2.2] - 2026-09-23
4
26
 
5
27
  ### Fixed
package/README.md CHANGED
@@ -47,7 +47,7 @@ OMK stores user configuration under `~/.omk/agent/` and project configuration un
47
47
  Install `omk-book-to-skill` to compile documents into reusable skills without adding Python extractors to OMK core:
48
48
 
49
49
  ```bash
50
- omk install npm:omk-book-to-skill@1.2.2
50
+ omk install npm:omk-book-to-skill@1.2.4
51
51
  ```
52
52
 
53
53
  It provides compile, update, and verification commands plus a local SHA-256 provenance manifest. See [Book to Skill](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/book-to-skill.md).
@@ -258,7 +258,7 @@ npm test
258
258
 
259
259
  See [Development](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/development.md) and [CONTRIBUTING.md](https://github.com/dmae97/omk/blob/main/CONTRIBUTING.md).
260
260
 
261
- Release notes: [v1.2.2](https://github.com/dmae97/omk/blob/main/.github/RELEASE_NOTES_v1.2.2.md).
261
+ Release notes: [v1.2.4](https://github.com/dmae97/omk/blob/main/.github/RELEASE_NOTES_v1.2.4.md).
262
262
 
263
263
  ## License
264
264
 
@@ -1 +1 @@
1
- {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,wBAAgB,SAAS,CAAC,cAAc,CAAC,EAAE,aAAa,EAAE,GAAG,IAAI,CAmMhE","sourcesContent":["import chalk from \"chalk\";\nimport { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from \"../config.ts\";\nimport type { ExtensionFlag } from \"../core/extensions/types.ts\";\n\nexport function printHelp(extensionFlags?: ExtensionFlag[]): void {\n\tlet extensionFlagsText = \"\";\n\tif (extensionFlags && extensionFlags.length > 0) {\n\t\textensionFlagsText = `\\n${chalk.bold(\"Extension CLI Flags:\")}\\n${extensionFlags\n\t\t\t.map((flag) => {\n\t\t\t\tconst value = flag.type === \"string\" ? \" <value>\" : \"\";\n\t\t\t\tconst description = flag.description ?? `Registered by ${flag.extensionPath}`;\n\t\t\t\treturn ` --${flag.name}${value}`.padEnd(30) + description;\n\t\t\t})\n\t\t\t.join(\"\\n\")}\\n`;\n\t}\n\t// CLI help is intentional stdout, not application debug logging.\n\t// pi-lens-ignore: no-console-except-error\n\tconsole.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools\n\n${chalk.bold(\"Usage:\")}\n ${APP_NAME} [options] [@files...] [messages...]\n\n${chalk.bold(\"Commands:\")}\n ${APP_NAME} init --global [--dry-run] Create missing user context documents without overwriting settings\n ${APP_NAME} install <source> [-l] Install extension source and add to settings\n ${APP_NAME} remove <source> [-l] Remove extension source from settings\n ${APP_NAME} uninstall <source> [-l] Alias for remove\n ${APP_NAME} update [source|self|omk] Update OMK and installed extensions\n ${APP_NAME} list List installed extensions from settings\n ${APP_NAME} package doctor <source> Diagnose omk/pi manifest and runtime compatibility as JSON\n ${APP_NAME} config Open TUI to enable/disable package resources\n ${APP_NAME} quota Manage CodexBar quota connector and summaries\n ${APP_NAME} provider doctor <id> Diagnose a provider as sanitized JSON (--probe-model adds an\n opt-in Level-2 generative probe that may incur provider costs)\n ${APP_NAME} session doctor Inspect or safely repair one session (--session, --repair, --dry-run)\n ${APP_NAME} doctor resources [--json] [--report] Inspect current pressure or aggregate local observations\n ${APP_NAME} doctor adaptorch [--json] Verify ADAPTORCH_API_KEY reaches the AdaptOrch API (0 ok, 1 unset, 2 failed)\n ${APP_NAME} router-feedback compile-bias Compile this repository's bounded router bias snapshot\n ${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list\n\n${chalk.bold(\"Options:\")}\n --provider <name> Provider name (default: google)\n --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")\n --model-contract <file> Pin an optional JSON model/effort/output dispatch policy\n --api-key <key> API key (defaults to env vars)\n --system-prompt <text> System prompt (default: coding assistant prompt)\n --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)\n --mode <mode> Output mode: text (default), json, rpc, or acp (conversation-only)\n --print, -p Non-interactive mode: process prompt and exit\n --continue, -c Continue previous session\n --resume, -r Select a session to resume\n --session <path|id> Use specific session file or partial UUID\n --session-id <id> Use exact project session ID, creating it if missing\n --fork <path|id> Fork specific session file or partial UUID into a new session\n --session-dir <dir> Directory for session storage and lookup\n --no-session Don't save session (ephemeral)\n --name, -n <name> Set session display name\n --models <patterns> Comma-separated model patterns for Ctrl+P cycling\n Supports globs (anthropic/*, *sonnet*) and fuzzy matching\n --no-tools, -nt Disable all tools by default (built-in and extension)\n --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled\n --tools, -t <tools> Comma-separated allowlist of tool names to enable\n Applies to built-in, extension, and custom tools\n --exclude-tools, -xt <tools> Comma-separated denylist of tool names to disable\n Applies to built-in, extension, and custom tools\n --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh, max, ultra\n --extension, -e <path> Load an extension file (can be used multiple times)\n --no-extensions, -ne Disable extension discovery (explicit -e paths still work)\n --skill <path> Load a skill file or directory (can be used multiple times)\n --no-skills, -ns Disable skills discovery and loading\n --prompt-template <path> Load a prompt template file or directory (can be used multiple times)\n --no-prompt-templates, -np Disable prompt template discovery and loading\n --theme <path> Load a theme file or directory (can be used multiple times)\n --no-themes Disable theme discovery and loading\n --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading\n --export <file> Export session file to HTML and exit\n --list-models [search] List available models (with optional fuzzy search)\n --verbose Force verbose startup (overrides quietStartup setting)\n --offline Disable startup network operations (same as OMK_OFFLINE=1)\n --help, -h Show this help\n --version, -v Show version number\n\nExtensions can register additional flags (e.g., --plan from plan-mode extension).${extensionFlagsText}\n\n${chalk.bold(\"Examples:\")}\n # Interactive mode\n ${APP_NAME}\n\n # Interactive mode with initial prompt\n ${APP_NAME} \"List all .ts files in src/\"\n\n # Include files in initial message\n ${APP_NAME} @prompt.md @image.png \"What color is the sky?\"\n\n # Non-interactive mode (process and exit)\n ${APP_NAME} -p \"List all .ts files in src/\"\n\n # Multiple messages (interactive)\n ${APP_NAME} \"Read package.json\" \"What dependencies do we have?\"\n\n # Continue previous session\n ${APP_NAME} --continue \"What did we discuss?\"\n\n # Start a named session\n ${APP_NAME} --name \"Refactor auth module\"\n\n # Use different model\n ${APP_NAME} --provider openai --model gpt-4o-mini \"Help me refactor this code\"\n\n # Use model with provider prefix (no --provider needed)\n ${APP_NAME} --model openai/gpt-4o \"Help me refactor this code\"\n\n # Use model with thinking level shorthand\n ${APP_NAME} --model sonnet:high \"Solve this complex problem\"\n\n # Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o\n\n # Limit to a specific provider with glob pattern\n ${APP_NAME} --models \"github-copilot/*\"\n\n # Cycle models with fixed thinking levels\n ${APP_NAME} --models sonnet:high,haiku:low\n\n # Start with a specific thinking level\n ${APP_NAME} --thinking high \"Solve this complex problem\"\n\n # Read-only mode (no file modifications possible)\n ${APP_NAME} --tools read,grep,find,ls -p \"Review the code in src/\"\n\n # Disable one tool while keeping the rest available\n ${APP_NAME} --exclude-tools ask_question\n\n # Export a session file to HTML\n ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl\n ${APP_NAME} --export session.jsonl output.html\n\n${chalk.bold(\"Environment Variables:\")}\n ANTHROPIC_API_KEY - Anthropic Claude API key\n ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)\n ANT_LING_API_KEY - Ant Ling API key\n OPENAI_API_KEY - OpenAI GPT API key\n AZURE_OPENAI_API_KEY - Azure OpenAI API key\n AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)\n AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)\n AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)\n AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)\n DEEPSEEK_API_KEY - DeepSeek API key\n DEVIN_API_KEY - Devin CLI subscription session token (or /login devin)\n NVIDIA_API_KEY - NVIDIA NIM API key\n GEMINI_API_KEY - Google Gemini API key\n GROQ_API_KEY - Groq API key\n CEREBRAS_API_KEY - Cerebras API key\n XAI_API_KEY - xAI Grok API key\n FIREWORKS_API_KEY - Fireworks API key\n TOGETHER_API_KEY - Together AI API key\n OPENROUTER_API_KEY - OpenRouter API key\n AI_GATEWAY_API_KEY - Vercel AI Gateway API key\n ZAI_API_KEY - ZAI API key\n ZAI_CODING_CN_API_KEY - ZAI Coding Plan API key (China)\n MISTRAL_API_KEY - Mistral API key\n MINIMAX_API_KEY - MiniMax API key\n MOONSHOT_API_KEY - Moonshot AI API key\n META_API_KEY - Meta Model API key (Muse Spark); META_MODEL_API_KEY/MODEL_API_KEY also read\n OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key\n KIMI_API_KEY - Kimi For Coding API key\n WORKBUDDY_API_KEY - WorkBuddy (Tencent CodeBuddy) plan key\n CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)\n CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)\n CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)\n XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)\n XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)\n XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)\n XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)\n ZYLOO_API_KEY - Zyloo API key\n AWS_PROFILE - AWS profile for Amazon Bedrock\n AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock\n AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock\n AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)\n AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)\n ${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)\n ${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)\n OMK_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)\n OMK_TOOL_SCHEDULER - Tool scheduler override: dag-v2 or waves-v1 rollback\n OMK_OFFLINE - Disable startup network operations when set to 1/true/yes\n OMK_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no\n OMK_SHARE_VIEWER_URL - Base URL for /share command (default: https://omk.dev/session/)\n ADAPTORCH_API_KEY - AdaptOrch API key (optional; enables the doctor adaptorch check)\n ADAPTORCH_API_URL - AdaptOrch API base URL (default: https://api.adaptorch.com)\n\n${chalk.bold(\"Built-in Tool Names:\")}\n read - Read file contents\n bash - Execute bash commands\n edit - Edit files with find/replace\n write - Write files (creates/overwrites)\n grep - Search file contents (read-only, off by default)\n find - Find files by glob pattern (read-only, off by default)\n ls - List directory contents (read-only, off by default)\n`);\n}\n"]}
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,wBAAgB,SAAS,CAAC,cAAc,CAAC,EAAE,aAAa,EAAE,GAAG,IAAI,CAqMhE","sourcesContent":["import chalk from \"chalk\";\nimport { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from \"../config.ts\";\nimport type { ExtensionFlag } from \"../core/extensions/types.ts\";\n\nexport function printHelp(extensionFlags?: ExtensionFlag[]): void {\n\tlet extensionFlagsText = \"\";\n\tif (extensionFlags && extensionFlags.length > 0) {\n\t\textensionFlagsText = `\\n${chalk.bold(\"Extension CLI Flags:\")}\\n${extensionFlags\n\t\t\t.map((flag) => {\n\t\t\t\tconst value = flag.type === \"string\" ? \" <value>\" : \"\";\n\t\t\t\tconst description = flag.description ?? `Registered by ${flag.extensionPath}`;\n\t\t\t\treturn ` --${flag.name}${value}`.padEnd(30) + description;\n\t\t\t})\n\t\t\t.join(\"\\n\")}\\n`;\n\t}\n\t// CLI help is intentional stdout, not application debug logging.\n\t// pi-lens-ignore: no-console-except-error\n\tconsole.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools\n\n${chalk.bold(\"Usage:\")}\n ${APP_NAME} [options] [@files...] [messages...]\n\n${chalk.bold(\"Commands:\")}\n ${APP_NAME} init --global [--dry-run] Create missing user context documents without overwriting settings\n ${APP_NAME} install <source> [-l] Install extension source and add to settings\n ${APP_NAME} remove <source> [-l] Remove extension source from settings\n ${APP_NAME} uninstall <source> [-l] Alias for remove\n ${APP_NAME} update [source|self|omk] Update OMK and installed extensions\n ${APP_NAME} list List installed extensions from settings\n ${APP_NAME} package doctor <source> Diagnose omk/pi manifest and runtime compatibility as JSON\n ${APP_NAME} config Open TUI to enable/disable package resources\n ${APP_NAME} quota Manage CodexBar quota connector and summaries\n ${APP_NAME} provider doctor <id> Diagnose a provider as sanitized JSON (--probe-model adds an\n opt-in Level-2 generative probe that may incur provider costs)\n ${APP_NAME} provider adopt [<id>] Copy an existing Codex CLI / Claude Code CLI login into OMK's\n store (--from <source>, --dry-run, --status, --json)\n ${APP_NAME} session doctor Inspect or safely repair one session (--session, --repair, --dry-run)\n ${APP_NAME} doctor resources [--json] [--report] Inspect current pressure or aggregate local observations\n ${APP_NAME} doctor adaptorch [--json] Verify ADAPTORCH_API_KEY reaches the AdaptOrch API (0 ok, 1 unset, 2 failed)\n ${APP_NAME} router-feedback compile-bias Compile this repository's bounded router bias snapshot\n ${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list\n\n${chalk.bold(\"Options:\")}\n --provider <name> Provider name (default: google)\n --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")\n --model-contract <file> Pin an optional JSON model/effort/output dispatch policy\n --api-key <key> API key (defaults to env vars)\n --system-prompt <text> System prompt (default: coding assistant prompt)\n --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)\n --mode <mode> Output mode: text (default), json, rpc, or acp (conversation-only)\n --print, -p Non-interactive mode: process prompt and exit\n --continue, -c Continue previous session\n --resume, -r Select a session to resume\n --session <path|id> Use specific session file or partial UUID\n --session-id <id> Use exact project session ID, creating it if missing\n --fork <path|id> Fork specific session file or partial UUID into a new session\n --session-dir <dir> Directory for session storage and lookup\n --no-session Don't save session (ephemeral)\n --name, -n <name> Set session display name\n --models <patterns> Comma-separated model patterns for Ctrl+P cycling\n Supports globs (anthropic/*, *sonnet*) and fuzzy matching\n --no-tools, -nt Disable all tools by default (built-in and extension)\n --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled\n --tools, -t <tools> Comma-separated allowlist of tool names to enable\n Applies to built-in, extension, and custom tools\n --exclude-tools, -xt <tools> Comma-separated denylist of tool names to disable\n Applies to built-in, extension, and custom tools\n --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh, max, ultra\n --extension, -e <path> Load an extension file (can be used multiple times)\n --no-extensions, -ne Disable extension discovery (explicit -e paths still work)\n --skill <path> Load a skill file or directory (can be used multiple times)\n --no-skills, -ns Disable skills discovery and loading\n --prompt-template <path> Load a prompt template file or directory (can be used multiple times)\n --no-prompt-templates, -np Disable prompt template discovery and loading\n --theme <path> Load a theme file or directory (can be used multiple times)\n --no-themes Disable theme discovery and loading\n --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading\n --export <file> Export session file to HTML and exit\n --list-models [search] List available models (with optional fuzzy search)\n --verbose Force verbose startup (overrides quietStartup setting)\n --offline Disable startup network operations (same as OMK_OFFLINE=1)\n --help, -h Show this help\n --version, -v Show version number\n\nExtensions can register additional flags (e.g., --plan from plan-mode extension).${extensionFlagsText}\n\n${chalk.bold(\"Examples:\")}\n # Interactive mode\n ${APP_NAME}\n\n # Interactive mode with initial prompt\n ${APP_NAME} \"List all .ts files in src/\"\n\n # Include files in initial message\n ${APP_NAME} @prompt.md @image.png \"What color is the sky?\"\n\n # Non-interactive mode (process and exit)\n ${APP_NAME} -p \"List all .ts files in src/\"\n\n # Multiple messages (interactive)\n ${APP_NAME} \"Read package.json\" \"What dependencies do we have?\"\n\n # Continue previous session\n ${APP_NAME} --continue \"What did we discuss?\"\n\n # Start a named session\n ${APP_NAME} --name \"Refactor auth module\"\n\n # Use different model\n ${APP_NAME} --provider openai --model gpt-4o-mini \"Help me refactor this code\"\n\n # Use model with provider prefix (no --provider needed)\n ${APP_NAME} --model openai/gpt-4o \"Help me refactor this code\"\n\n # Use model with thinking level shorthand\n ${APP_NAME} --model sonnet:high \"Solve this complex problem\"\n\n # Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o\n\n # Limit to a specific provider with glob pattern\n ${APP_NAME} --models \"github-copilot/*\"\n\n # Cycle models with fixed thinking levels\n ${APP_NAME} --models sonnet:high,haiku:low\n\n # Start with a specific thinking level\n ${APP_NAME} --thinking high \"Solve this complex problem\"\n\n # Read-only mode (no file modifications possible)\n ${APP_NAME} --tools read,grep,find,ls -p \"Review the code in src/\"\n\n # Disable one tool while keeping the rest available\n ${APP_NAME} --exclude-tools ask_question\n\n # Export a session file to HTML\n ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl\n ${APP_NAME} --export session.jsonl output.html\n\n${chalk.bold(\"Environment Variables:\")}\n ANTHROPIC_API_KEY - Anthropic Claude API key\n ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)\n ANT_LING_API_KEY - Ant Ling API key\n OPENAI_API_KEY - OpenAI GPT API key\n AZURE_OPENAI_API_KEY - Azure OpenAI API key\n AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)\n AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)\n AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)\n AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)\n DEEPSEEK_API_KEY - DeepSeek API key\n DEVIN_API_KEY - Devin CLI subscription session token (or /login devin)\n NVIDIA_API_KEY - NVIDIA NIM API key\n GEMINI_API_KEY - Google Gemini API key\n GROQ_API_KEY - Groq API key\n CEREBRAS_API_KEY - Cerebras API key\n XAI_API_KEY - xAI Grok API key\n FIREWORKS_API_KEY - Fireworks API key\n TOGETHER_API_KEY - Together AI API key\n OPENROUTER_API_KEY - OpenRouter API key\n AI_GATEWAY_API_KEY - Vercel AI Gateway API key\n ZAI_API_KEY - ZAI API key\n ZAI_CODING_CN_API_KEY - ZAI Coding Plan API key (China)\n MISTRAL_API_KEY - Mistral API key\n MINIMAX_API_KEY - MiniMax API key\n MOONSHOT_API_KEY - Moonshot AI API key\n META_API_KEY - Meta Model API key (Muse Spark); META_MODEL_API_KEY/MODEL_API_KEY also read\n OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key\n KIMI_API_KEY - Kimi For Coding API key\n WORKBUDDY_API_KEY - WorkBuddy (Tencent CodeBuddy) plan key\n CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)\n CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)\n CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)\n XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)\n XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)\n XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)\n XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)\n ZYLOO_API_KEY - Zyloo API key\n AWS_PROFILE - AWS profile for Amazon Bedrock\n AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock\n AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock\n AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)\n AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)\n ${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)\n ${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)\n OMK_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)\n OMK_TOOL_SCHEDULER - Tool scheduler override: dag-v2 or waves-v1 rollback\n OMK_OFFLINE - Disable startup network operations when set to 1/true/yes\n OMK_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no\n OMK_SHARE_VIEWER_URL - Base URL for /share command (default: https://omk.dev/session/)\n ADAPTORCH_API_KEY - AdaptOrch API key (optional; enables the doctor adaptorch check)\n ADAPTORCH_API_URL - AdaptOrch API base URL (default: https://api.adaptorch.com)\n\n${chalk.bold(\"Built-in Tool Names:\")}\n read - Read file contents\n bash - Execute bash commands\n edit - Edit files with find/replace\n write - Write files (creates/overwrites)\n grep - Search file contents (read-only, off by default)\n find - Find files by glob pattern (read-only, off by default)\n ls - List directory contents (read-only, off by default)\n`);\n}\n"]}
package/dist/cli/help.js CHANGED
@@ -30,6 +30,8 @@ ${chalk.bold("Commands:")}
30
30
  ${APP_NAME} quota Manage CodexBar quota connector and summaries
31
31
  ${APP_NAME} provider doctor <id> Diagnose a provider as sanitized JSON (--probe-model adds an
32
32
  opt-in Level-2 generative probe that may incur provider costs)
33
+ ${APP_NAME} provider adopt [<id>] Copy an existing Codex CLI / Claude Code CLI login into OMK's
34
+ store (--from <source>, --dry-run, --status, --json)
33
35
  ${APP_NAME} session doctor Inspect or safely repair one session (--session, --repair, --dry-run)
34
36
  ${APP_NAME} doctor resources [--json] [--report] Inspect current pressure or aggregate local observations
35
37
  ${APP_NAME} doctor adaptorch [--json] Verify ADAPTORCH_API_KEY reaches the AdaptOrch API (0 ok, 1 unset, 2 failed)
@@ -1 +1 @@
1
- {"version":3,"file":"help.js","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGzF,MAAM,UAAU,SAAS,CAAC,cAAgC,EAAQ;IACjE,IAAI,kBAAkB,GAAG,EAAE,CAAC;IAC5B,IAAI,cAAc,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjD,kBAAkB,GAAG,KAAK,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,KAAK,cAAc;aAC7E,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YACd,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,iBAAiB,IAAI,CAAC,aAAa,EAAE,CAAC;YAC9E,OAAO,OAAO,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC;QAAA,CAC3D,CAAC;aACD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IAClB,CAAC;IACD,iEAAiE;IACjE,0CAA0C;IAC1C,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;;EAElC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;IAClB,QAAQ;;EAEV,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC;IACrB,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;;IAER,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;;EAEV,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mFA0C2D,kBAAkB;;EAEnG,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC;;IAErB,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ,eAAe,eAAe;IACtC,QAAQ;;EAEV,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2ClC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,mCAAmC,eAAe;IAC1E,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;;;;;;;;;EAS5B,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC;;;;;;;;CAQnC,CAAC,CAAC;AAAA,CACF","sourcesContent":["import chalk from \"chalk\";\nimport { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from \"../config.ts\";\nimport type { ExtensionFlag } from \"../core/extensions/types.ts\";\n\nexport function printHelp(extensionFlags?: ExtensionFlag[]): void {\n\tlet extensionFlagsText = \"\";\n\tif (extensionFlags && extensionFlags.length > 0) {\n\t\textensionFlagsText = `\\n${chalk.bold(\"Extension CLI Flags:\")}\\n${extensionFlags\n\t\t\t.map((flag) => {\n\t\t\t\tconst value = flag.type === \"string\" ? \" <value>\" : \"\";\n\t\t\t\tconst description = flag.description ?? `Registered by ${flag.extensionPath}`;\n\t\t\t\treturn ` --${flag.name}${value}`.padEnd(30) + description;\n\t\t\t})\n\t\t\t.join(\"\\n\")}\\n`;\n\t}\n\t// CLI help is intentional stdout, not application debug logging.\n\t// pi-lens-ignore: no-console-except-error\n\tconsole.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools\n\n${chalk.bold(\"Usage:\")}\n ${APP_NAME} [options] [@files...] [messages...]\n\n${chalk.bold(\"Commands:\")}\n ${APP_NAME} init --global [--dry-run] Create missing user context documents without overwriting settings\n ${APP_NAME} install <source> [-l] Install extension source and add to settings\n ${APP_NAME} remove <source> [-l] Remove extension source from settings\n ${APP_NAME} uninstall <source> [-l] Alias for remove\n ${APP_NAME} update [source|self|omk] Update OMK and installed extensions\n ${APP_NAME} list List installed extensions from settings\n ${APP_NAME} package doctor <source> Diagnose omk/pi manifest and runtime compatibility as JSON\n ${APP_NAME} config Open TUI to enable/disable package resources\n ${APP_NAME} quota Manage CodexBar quota connector and summaries\n ${APP_NAME} provider doctor <id> Diagnose a provider as sanitized JSON (--probe-model adds an\n opt-in Level-2 generative probe that may incur provider costs)\n ${APP_NAME} session doctor Inspect or safely repair one session (--session, --repair, --dry-run)\n ${APP_NAME} doctor resources [--json] [--report] Inspect current pressure or aggregate local observations\n ${APP_NAME} doctor adaptorch [--json] Verify ADAPTORCH_API_KEY reaches the AdaptOrch API (0 ok, 1 unset, 2 failed)\n ${APP_NAME} router-feedback compile-bias Compile this repository's bounded router bias snapshot\n ${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list\n\n${chalk.bold(\"Options:\")}\n --provider <name> Provider name (default: google)\n --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")\n --model-contract <file> Pin an optional JSON model/effort/output dispatch policy\n --api-key <key> API key (defaults to env vars)\n --system-prompt <text> System prompt (default: coding assistant prompt)\n --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)\n --mode <mode> Output mode: text (default), json, rpc, or acp (conversation-only)\n --print, -p Non-interactive mode: process prompt and exit\n --continue, -c Continue previous session\n --resume, -r Select a session to resume\n --session <path|id> Use specific session file or partial UUID\n --session-id <id> Use exact project session ID, creating it if missing\n --fork <path|id> Fork specific session file or partial UUID into a new session\n --session-dir <dir> Directory for session storage and lookup\n --no-session Don't save session (ephemeral)\n --name, -n <name> Set session display name\n --models <patterns> Comma-separated model patterns for Ctrl+P cycling\n Supports globs (anthropic/*, *sonnet*) and fuzzy matching\n --no-tools, -nt Disable all tools by default (built-in and extension)\n --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled\n --tools, -t <tools> Comma-separated allowlist of tool names to enable\n Applies to built-in, extension, and custom tools\n --exclude-tools, -xt <tools> Comma-separated denylist of tool names to disable\n Applies to built-in, extension, and custom tools\n --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh, max, ultra\n --extension, -e <path> Load an extension file (can be used multiple times)\n --no-extensions, -ne Disable extension discovery (explicit -e paths still work)\n --skill <path> Load a skill file or directory (can be used multiple times)\n --no-skills, -ns Disable skills discovery and loading\n --prompt-template <path> Load a prompt template file or directory (can be used multiple times)\n --no-prompt-templates, -np Disable prompt template discovery and loading\n --theme <path> Load a theme file or directory (can be used multiple times)\n --no-themes Disable theme discovery and loading\n --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading\n --export <file> Export session file to HTML and exit\n --list-models [search] List available models (with optional fuzzy search)\n --verbose Force verbose startup (overrides quietStartup setting)\n --offline Disable startup network operations (same as OMK_OFFLINE=1)\n --help, -h Show this help\n --version, -v Show version number\n\nExtensions can register additional flags (e.g., --plan from plan-mode extension).${extensionFlagsText}\n\n${chalk.bold(\"Examples:\")}\n # Interactive mode\n ${APP_NAME}\n\n # Interactive mode with initial prompt\n ${APP_NAME} \"List all .ts files in src/\"\n\n # Include files in initial message\n ${APP_NAME} @prompt.md @image.png \"What color is the sky?\"\n\n # Non-interactive mode (process and exit)\n ${APP_NAME} -p \"List all .ts files in src/\"\n\n # Multiple messages (interactive)\n ${APP_NAME} \"Read package.json\" \"What dependencies do we have?\"\n\n # Continue previous session\n ${APP_NAME} --continue \"What did we discuss?\"\n\n # Start a named session\n ${APP_NAME} --name \"Refactor auth module\"\n\n # Use different model\n ${APP_NAME} --provider openai --model gpt-4o-mini \"Help me refactor this code\"\n\n # Use model with provider prefix (no --provider needed)\n ${APP_NAME} --model openai/gpt-4o \"Help me refactor this code\"\n\n # Use model with thinking level shorthand\n ${APP_NAME} --model sonnet:high \"Solve this complex problem\"\n\n # Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o\n\n # Limit to a specific provider with glob pattern\n ${APP_NAME} --models \"github-copilot/*\"\n\n # Cycle models with fixed thinking levels\n ${APP_NAME} --models sonnet:high,haiku:low\n\n # Start with a specific thinking level\n ${APP_NAME} --thinking high \"Solve this complex problem\"\n\n # Read-only mode (no file modifications possible)\n ${APP_NAME} --tools read,grep,find,ls -p \"Review the code in src/\"\n\n # Disable one tool while keeping the rest available\n ${APP_NAME} --exclude-tools ask_question\n\n # Export a session file to HTML\n ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl\n ${APP_NAME} --export session.jsonl output.html\n\n${chalk.bold(\"Environment Variables:\")}\n ANTHROPIC_API_KEY - Anthropic Claude API key\n ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)\n ANT_LING_API_KEY - Ant Ling API key\n OPENAI_API_KEY - OpenAI GPT API key\n AZURE_OPENAI_API_KEY - Azure OpenAI API key\n AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)\n AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)\n AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)\n AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)\n DEEPSEEK_API_KEY - DeepSeek API key\n DEVIN_API_KEY - Devin CLI subscription session token (or /login devin)\n NVIDIA_API_KEY - NVIDIA NIM API key\n GEMINI_API_KEY - Google Gemini API key\n GROQ_API_KEY - Groq API key\n CEREBRAS_API_KEY - Cerebras API key\n XAI_API_KEY - xAI Grok API key\n FIREWORKS_API_KEY - Fireworks API key\n TOGETHER_API_KEY - Together AI API key\n OPENROUTER_API_KEY - OpenRouter API key\n AI_GATEWAY_API_KEY - Vercel AI Gateway API key\n ZAI_API_KEY - ZAI API key\n ZAI_CODING_CN_API_KEY - ZAI Coding Plan API key (China)\n MISTRAL_API_KEY - Mistral API key\n MINIMAX_API_KEY - MiniMax API key\n MOONSHOT_API_KEY - Moonshot AI API key\n META_API_KEY - Meta Model API key (Muse Spark); META_MODEL_API_KEY/MODEL_API_KEY also read\n OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key\n KIMI_API_KEY - Kimi For Coding API key\n WORKBUDDY_API_KEY - WorkBuddy (Tencent CodeBuddy) plan key\n CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)\n CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)\n CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)\n XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)\n XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)\n XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)\n XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)\n ZYLOO_API_KEY - Zyloo API key\n AWS_PROFILE - AWS profile for Amazon Bedrock\n AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock\n AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock\n AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)\n AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)\n ${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)\n ${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)\n OMK_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)\n OMK_TOOL_SCHEDULER - Tool scheduler override: dag-v2 or waves-v1 rollback\n OMK_OFFLINE - Disable startup network operations when set to 1/true/yes\n OMK_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no\n OMK_SHARE_VIEWER_URL - Base URL for /share command (default: https://omk.dev/session/)\n ADAPTORCH_API_KEY - AdaptOrch API key (optional; enables the doctor adaptorch check)\n ADAPTORCH_API_URL - AdaptOrch API base URL (default: https://api.adaptorch.com)\n\n${chalk.bold(\"Built-in Tool Names:\")}\n read - Read file contents\n bash - Execute bash commands\n edit - Edit files with find/replace\n write - Write files (creates/overwrites)\n grep - Search file contents (read-only, off by default)\n find - Find files by glob pattern (read-only, off by default)\n ls - List directory contents (read-only, off by default)\n`);\n}\n"]}
1
+ {"version":3,"file":"help.js","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGzF,MAAM,UAAU,SAAS,CAAC,cAAgC,EAAQ;IACjE,IAAI,kBAAkB,GAAG,EAAE,CAAC;IAC5B,IAAI,cAAc,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjD,kBAAkB,GAAG,KAAK,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,KAAK,cAAc;aAC7E,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YACd,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,iBAAiB,IAAI,CAAC,aAAa,EAAE,CAAC;YAC9E,OAAO,OAAO,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC;QAAA,CAC3D,CAAC;aACD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IAClB,CAAC;IACD,iEAAiE;IACjE,0CAA0C;IAC1C,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;;EAElC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;IAClB,QAAQ;;EAEV,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC;IACrB,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;;IAER,QAAQ;;IAER,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;;EAEV,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mFA0C2D,kBAAkB;;EAEnG,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC;;IAErB,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ;;;IAGR,QAAQ,eAAe,eAAe;IACtC,QAAQ;;EAEV,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2ClC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,mCAAmC,eAAe;IAC1E,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;;;;;;;;;EAS5B,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC;;;;;;;;CAQnC,CAAC,CAAC;AAAA,CACF","sourcesContent":["import chalk from \"chalk\";\nimport { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from \"../config.ts\";\nimport type { ExtensionFlag } from \"../core/extensions/types.ts\";\n\nexport function printHelp(extensionFlags?: ExtensionFlag[]): void {\n\tlet extensionFlagsText = \"\";\n\tif (extensionFlags && extensionFlags.length > 0) {\n\t\textensionFlagsText = `\\n${chalk.bold(\"Extension CLI Flags:\")}\\n${extensionFlags\n\t\t\t.map((flag) => {\n\t\t\t\tconst value = flag.type === \"string\" ? \" <value>\" : \"\";\n\t\t\t\tconst description = flag.description ?? `Registered by ${flag.extensionPath}`;\n\t\t\t\treturn ` --${flag.name}${value}`.padEnd(30) + description;\n\t\t\t})\n\t\t\t.join(\"\\n\")}\\n`;\n\t}\n\t// CLI help is intentional stdout, not application debug logging.\n\t// pi-lens-ignore: no-console-except-error\n\tconsole.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools\n\n${chalk.bold(\"Usage:\")}\n ${APP_NAME} [options] [@files...] [messages...]\n\n${chalk.bold(\"Commands:\")}\n ${APP_NAME} init --global [--dry-run] Create missing user context documents without overwriting settings\n ${APP_NAME} install <source> [-l] Install extension source and add to settings\n ${APP_NAME} remove <source> [-l] Remove extension source from settings\n ${APP_NAME} uninstall <source> [-l] Alias for remove\n ${APP_NAME} update [source|self|omk] Update OMK and installed extensions\n ${APP_NAME} list List installed extensions from settings\n ${APP_NAME} package doctor <source> Diagnose omk/pi manifest and runtime compatibility as JSON\n ${APP_NAME} config Open TUI to enable/disable package resources\n ${APP_NAME} quota Manage CodexBar quota connector and summaries\n ${APP_NAME} provider doctor <id> Diagnose a provider as sanitized JSON (--probe-model adds an\n opt-in Level-2 generative probe that may incur provider costs)\n ${APP_NAME} provider adopt [<id>] Copy an existing Codex CLI / Claude Code CLI login into OMK's\n store (--from <source>, --dry-run, --status, --json)\n ${APP_NAME} session doctor Inspect or safely repair one session (--session, --repair, --dry-run)\n ${APP_NAME} doctor resources [--json] [--report] Inspect current pressure or aggregate local observations\n ${APP_NAME} doctor adaptorch [--json] Verify ADAPTORCH_API_KEY reaches the AdaptOrch API (0 ok, 1 unset, 2 failed)\n ${APP_NAME} router-feedback compile-bias Compile this repository's bounded router bias snapshot\n ${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list\n\n${chalk.bold(\"Options:\")}\n --provider <name> Provider name (default: google)\n --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")\n --model-contract <file> Pin an optional JSON model/effort/output dispatch policy\n --api-key <key> API key (defaults to env vars)\n --system-prompt <text> System prompt (default: coding assistant prompt)\n --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)\n --mode <mode> Output mode: text (default), json, rpc, or acp (conversation-only)\n --print, -p Non-interactive mode: process prompt and exit\n --continue, -c Continue previous session\n --resume, -r Select a session to resume\n --session <path|id> Use specific session file or partial UUID\n --session-id <id> Use exact project session ID, creating it if missing\n --fork <path|id> Fork specific session file or partial UUID into a new session\n --session-dir <dir> Directory for session storage and lookup\n --no-session Don't save session (ephemeral)\n --name, -n <name> Set session display name\n --models <patterns> Comma-separated model patterns for Ctrl+P cycling\n Supports globs (anthropic/*, *sonnet*) and fuzzy matching\n --no-tools, -nt Disable all tools by default (built-in and extension)\n --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled\n --tools, -t <tools> Comma-separated allowlist of tool names to enable\n Applies to built-in, extension, and custom tools\n --exclude-tools, -xt <tools> Comma-separated denylist of tool names to disable\n Applies to built-in, extension, and custom tools\n --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh, max, ultra\n --extension, -e <path> Load an extension file (can be used multiple times)\n --no-extensions, -ne Disable extension discovery (explicit -e paths still work)\n --skill <path> Load a skill file or directory (can be used multiple times)\n --no-skills, -ns Disable skills discovery and loading\n --prompt-template <path> Load a prompt template file or directory (can be used multiple times)\n --no-prompt-templates, -np Disable prompt template discovery and loading\n --theme <path> Load a theme file or directory (can be used multiple times)\n --no-themes Disable theme discovery and loading\n --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading\n --export <file> Export session file to HTML and exit\n --list-models [search] List available models (with optional fuzzy search)\n --verbose Force verbose startup (overrides quietStartup setting)\n --offline Disable startup network operations (same as OMK_OFFLINE=1)\n --help, -h Show this help\n --version, -v Show version number\n\nExtensions can register additional flags (e.g., --plan from plan-mode extension).${extensionFlagsText}\n\n${chalk.bold(\"Examples:\")}\n # Interactive mode\n ${APP_NAME}\n\n # Interactive mode with initial prompt\n ${APP_NAME} \"List all .ts files in src/\"\n\n # Include files in initial message\n ${APP_NAME} @prompt.md @image.png \"What color is the sky?\"\n\n # Non-interactive mode (process and exit)\n ${APP_NAME} -p \"List all .ts files in src/\"\n\n # Multiple messages (interactive)\n ${APP_NAME} \"Read package.json\" \"What dependencies do we have?\"\n\n # Continue previous session\n ${APP_NAME} --continue \"What did we discuss?\"\n\n # Start a named session\n ${APP_NAME} --name \"Refactor auth module\"\n\n # Use different model\n ${APP_NAME} --provider openai --model gpt-4o-mini \"Help me refactor this code\"\n\n # Use model with provider prefix (no --provider needed)\n ${APP_NAME} --model openai/gpt-4o \"Help me refactor this code\"\n\n # Use model with thinking level shorthand\n ${APP_NAME} --model sonnet:high \"Solve this complex problem\"\n\n # Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o\n\n # Limit to a specific provider with glob pattern\n ${APP_NAME} --models \"github-copilot/*\"\n\n # Cycle models with fixed thinking levels\n ${APP_NAME} --models sonnet:high,haiku:low\n\n # Start with a specific thinking level\n ${APP_NAME} --thinking high \"Solve this complex problem\"\n\n # Read-only mode (no file modifications possible)\n ${APP_NAME} --tools read,grep,find,ls -p \"Review the code in src/\"\n\n # Disable one tool while keeping the rest available\n ${APP_NAME} --exclude-tools ask_question\n\n # Export a session file to HTML\n ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl\n ${APP_NAME} --export session.jsonl output.html\n\n${chalk.bold(\"Environment Variables:\")}\n ANTHROPIC_API_KEY - Anthropic Claude API key\n ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)\n ANT_LING_API_KEY - Ant Ling API key\n OPENAI_API_KEY - OpenAI GPT API key\n AZURE_OPENAI_API_KEY - Azure OpenAI API key\n AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{resource}.openai.azure.com)\n AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)\n AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)\n AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)\n DEEPSEEK_API_KEY - DeepSeek API key\n DEVIN_API_KEY - Devin CLI subscription session token (or /login devin)\n NVIDIA_API_KEY - NVIDIA NIM API key\n GEMINI_API_KEY - Google Gemini API key\n GROQ_API_KEY - Groq API key\n CEREBRAS_API_KEY - Cerebras API key\n XAI_API_KEY - xAI Grok API key\n FIREWORKS_API_KEY - Fireworks API key\n TOGETHER_API_KEY - Together AI API key\n OPENROUTER_API_KEY - OpenRouter API key\n AI_GATEWAY_API_KEY - Vercel AI Gateway API key\n ZAI_API_KEY - ZAI API key\n ZAI_CODING_CN_API_KEY - ZAI Coding Plan API key (China)\n MISTRAL_API_KEY - Mistral API key\n MINIMAX_API_KEY - MiniMax API key\n MOONSHOT_API_KEY - Moonshot AI API key\n META_API_KEY - Meta Model API key (Muse Spark); META_MODEL_API_KEY/MODEL_API_KEY also read\n OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key\n KIMI_API_KEY - Kimi For Coding API key\n WORKBUDDY_API_KEY - WorkBuddy (Tencent CodeBuddy) plan key\n CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)\n CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)\n CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)\n XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)\n XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)\n XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)\n XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)\n ZYLOO_API_KEY - Zyloo API key\n AWS_PROFILE - AWS profile for Amazon Bedrock\n AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock\n AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock\n AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)\n AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)\n ${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)\n ${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)\n OMK_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)\n OMK_TOOL_SCHEDULER - Tool scheduler override: dag-v2 or waves-v1 rollback\n OMK_OFFLINE - Disable startup network operations when set to 1/true/yes\n OMK_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no\n OMK_SHARE_VIEWER_URL - Base URL for /share command (default: https://omk.dev/session/)\n ADAPTORCH_API_KEY - AdaptOrch API key (optional; enables the doctor adaptorch check)\n ADAPTORCH_API_URL - AdaptOrch API base URL (default: https://api.adaptorch.com)\n\n${chalk.bold(\"Built-in Tool Names:\")}\n read - Read file contents\n bash - Execute bash commands\n edit - Edit files with find/replace\n write - Write files (creates/overwrites)\n grep - Search file contents (read-only, off by default)\n find - Find files by glob pattern (read-only, off by default)\n ls - List directory contents (read-only, off by default)\n`);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-provider.d.ts","sourceRoot":"","sources":["../../src/commands/doctor-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { BlockList, isIP } from \"node:net\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { type Api, findEnvKeys, getModels, getProviders, type KnownProvider, type Model } from \"omk-ai\";\nimport { getAgentDir as getConfiguredAgentDir } from \"../config.ts\";\nimport { isConfigValueConfigured, resolveConfigValueOrThrow } from \"../core/resolve-config-value.ts\";\nimport { stripJsonComments } from \"../utils/json.ts\";\n\nexport type ProviderOrigin = \"native\" | \"custom-openai-compatible\" | \"local-proxy\" | \"unknown\";\nexport type ProviderDoctorLevel = 0 | 1 | 2;\nexport type ProviderDoctorCheckStatus = \"ok\" | \"fail\" | \"skipped\" | \"unsupported\";\nexport type DoctorErrorCategory = \"network\" | \"auth\" | \"model\" | \"config\" | \"server\" | \"unknown\";\n\n/** Exact endpoint-probe category model from the v0.90.9 design (§10.4). */\nexport type EndpointProbeCategory = \"ok\" | \"network\" | \"auth\" | \"unsupported-endpoint\" | \"server\";\n\nexport type EndpointProbeResult = {\n\treachable: boolean;\n\tauthenticated?: boolean;\n\tmodelsSupported?: boolean;\n\tstatus?: number;\n\tcategory: EndpointProbeCategory;\n};\n\nexport type ProviderDoctorCode =\n\t| \"ok\"\n\t| \"provider-not-found\"\n\t| \"models-config-invalid\"\n\t| \"auth-config-invalid\"\n\t| \"kimi-config-invalid\"\n\t| \"unsupported-level\"\n\t| \"model-provider-mismatch\"\n\t| \"api-missing\"\n\t| \"api-unsupported\"\n\t| \"base-url-missing\"\n\t| \"base-url-invalid\"\n\t| \"url-scheme-unsupported\"\n\t| \"origin-unsupported\"\n\t| \"address-policy-blocked\"\n\t| \"auth-missing\"\n\t| \"auth-materialization-failed\"\n\t| \"transport-required\"\n\t| \"address-pinning-required\"\n\t| \"redirect-blocked\"\n\t| \"authentication-failed\"\n\t| \"endpoint-unsupported\"\n\t| \"request-aborted\"\n\t| \"request-timeout\"\n\t| \"network-failure\"\n\t| \"server-error\"\n\t| \"probe-model-required\"\n\t| \"probe-model-conflict\"\n\t| \"unexpected-response\";\n\nexport interface ProviderDoctorEndpoint {\n\tbaseUrl?: string;\n\tapi?: string;\n\tmodelIds?: readonly string[];\n\tmodelId?: string;\n}\n\nexport type ProviderDoctorHeaderFactory = () => Headers | Promise<Headers>;\n\nexport type ProviderDoctorAuthHeaderResolver = (target: ResolvedProviderTarget) => Headers | Promise<Headers>;\n\n/** Fixed-message marker used to classify auth resolution without exposing its cause. */\nexport class ProviderDoctorAuthMaterializationError extends Error {\n\tconstructor() {\n\t\tsuper(\"provider doctor authentication could not be materialized\");\n\t\tthis.name = \"ProviderDoctorAuthMaterializationError\";\n\t}\n}\n\nexport interface ProviderDoctorCredentialBinding {\n\torigin: ProviderOrigin;\n\tsource: string;\n\tbaseUrl: string;\n\tapi: string;\n\tmodelId: string;\n}\n\nexport interface ProviderDoctorAuth {\n\tpresent: boolean;\n\tsource?: string;\n\t/** Materialized by an injected transport at request time only. */\n\tcreateHeaders?: ProviderDoctorHeaderFactory;\n\t/** Exact endpoint provenance that authorizes createHeaders. */\n\tbinding?: ProviderDoctorCredentialBinding;\n}\n\nexport interface ResolvedProviderTarget {\n\tproviderId: string;\n\torigin: ProviderOrigin;\n\tsource: string;\n\tendpoint: ProviderDoctorEndpoint;\n\tauth: ProviderDoctorAuth;\n}\n\n/** Compatibility name for callers using the design-document terminology. */\nexport type ResolvedProviderConfig = ResolvedProviderTarget;\n\nexport type ProviderDoctorAddressPolicy =\n\t| { kind: \"public\"; requireAddressPinning: true }\n\t| { kind: \"loopback-only\"; requireAddressPinning: false };\n\nexport interface ProviderDoctorTransportRequest {\n\turl: URL;\n\tmethod: \"GET\" | \"POST\";\n\tredirect: \"manual\";\n\tsignal: AbortSignal;\n\taddressPolicy: ProviderDoctorAddressPolicy;\n\tcreateHeaders: ProviderDoctorHeaderFactory;\n\t/** Bounded JSON payload; present only for opt-in Level-2 POST model probes. */\n\tbody?: string;\n\tcontentType?: \"application/json\";\n}\n\nexport interface ProviderDoctorTransportResponse {\n\tstatus: number;\n}\n\nexport interface ProviderDoctorTransport {\n\t/** Public requests are rejected unless the transport enforces DNS/address pinning. */\n\tpinsResolvedAddress?: boolean;\n\trequest(request: ProviderDoctorTransportRequest): Promise<ProviderDoctorTransportResponse>;\n}\n\nexport interface ProviderDoctorDependencies {\n\ttransport?: ProviderDoctorTransport;\n\t/** Level-2 only. Called by the transport immediately before each request is dispatched. */\n\tresolveAuthHeaders?: ProviderDoctorAuthHeaderResolver;\n}\n\nexport interface ProviderDoctorOptions extends ProviderDoctorDependencies {\n\tlevel?: ProviderDoctorLevel;\n\tmodelId?: string;\n\t/** Level-2 opt-in only: the model probed with one minimal-token generative request. */\n\tprobeModelId?: string;\n\ttimeoutMs?: number;\n\tsignal?: AbortSignal;\n\tagentDir?: string;\n\t/** Path to the Kimi CLI config TOML consulted for the kimi-coding provider. Defaults to ~/.kimi/config.toml. */\n\tkimiConfigPath?: string;\n}\n\nexport interface ProviderDoctorCheck {\n\tname: string;\n\tstatus: ProviderDoctorCheckStatus;\n\tcode: ProviderDoctorCode;\n\tcategory?: DoctorErrorCategory;\n\tmessage?: string;\n\t/** Exact-category endpoint classification for network probe checks. */\n\tprobe?: EndpointProbeResult;\n}\n\nexport interface ProviderDoctorResult {\n\tprovider: string;\n\tstatus: \"ok\" | \"fail\";\n\tlevel: ProviderDoctorLevel;\n\torigin: ProviderOrigin;\n\tsource: string;\n\t/** Sanitized URL: userinfo, query, and fragment are always removed. */\n\ttargetUrl?: string;\n\t/** Backwards-compatible alias of targetUrl. */\n\tbaseUrl?: string;\n\tapi?: string;\n\tmodelId?: string;\n\tauthPresent: boolean;\n\t/** True only when an opt-in Level-2 model probe was dispatched; the request may incur provider costs. */\n\tcostWarning?: boolean;\n\tchecks: ProviderDoctorCheck[];\n\terror?: {\n\t\tcategory: DoctorErrorCategory;\n\t\tcode: ProviderDoctorCode;\n\t\tmessage: string;\n\t};\n}\n\ninterface ModelEndpointConfig {\n\tid: string;\n\tbaseUrl?: string;\n\tapi?: string;\n}\n\ninterface ProviderConfig {\n\tbaseUrl?: string;\n\tapiKey?: string;\n\tapi?: string;\n\theaders?: Record<string, string>;\n\tmodels?: ModelEndpointConfig[];\n}\n\ninterface ModelsConfig {\n\tproviders: Record<string, ProviderConfig>;\n}\n\ninterface AuthConfigEntry {\n\ttype: \"api_key\" | \"oauth\";\n}\n\ntype AuthConfig = Record<string, AuthConfigEntry>;\n\ntype LoadedConfig<T> = { kind: \"missing\" } | { kind: \"invalid\" } | { kind: \"ok\"; value: T };\n\ntype ProbeFailure = {\n\tcategory: DoctorErrorCategory;\n\tcode: ProviderDoctorCode;\n\tmessage: string;\n};\n\ninterface ProbeOutcome {\n\tcheck: ProviderDoctorCheck;\n\tfailure?: ProbeFailure;\n}\n\nconst DEFAULT_TIMEOUT_MS = 5_000;\nconst KNOWN_APIS = new Set<string>([\n\t\"openai-completions\",\n\t\"mistral-conversations\",\n\t\"openai-responses\",\n\t\"azure-openai-responses\",\n\t\"openai-codex-responses\",\n\t\"anthropic-messages\",\n\t\"bedrock-converse-stream\",\n\t\"google-generative-ai\",\n\t\"google-vertex\",\n]);\nconst OPENAI_COMPATIBLE_APIS = new Set<string>([\"openai-completions\", \"openai-responses\"]);\nconst ABORTED = Symbol(\"provider-doctor-aborted\");\n\n// Family-specific lists: node BlockList canonicalizes IPv4 checks to the v4-mapped v6 form,\n// so a combined list containing ::ffff:0:0/96 would block every IPv4 address.\nconst blockedPublicV4 = new BlockList();\nfor (const [address, prefix] of [\n\t[\"0.0.0.0\", 8],\n\t[\"10.0.0.0\", 8],\n\t[\"100.64.0.0\", 10],\n\t[\"127.0.0.0\", 8],\n\t[\"169.254.0.0\", 16],\n\t[\"172.16.0.0\", 12],\n\t[\"192.168.0.0\", 16],\n\t[\"224.0.0.0\", 4],\n\t[\"240.0.0.0\", 4],\n] as const) {\n\tblockedPublicV4.addSubnet(address, prefix, \"ipv4\");\n}\n// ::ffff:0:0/96 stays here on purpose: a v4-mapped IPv6 literal or DNS answer is anomalous\n// for a public origin and is rejected wholesale (fail closed), covering hex-form mappings too.\nconst blockedPublicV6 = new BlockList();\nfor (const [address, prefix] of [\n\t[\"::\", 128],\n\t[\"::1\", 128],\n\t[\"::ffff:0:0\", 96],\n\t[\"fc00::\", 7],\n\t[\"fe80::\", 10],\n\t[\"ff00::\", 8],\n] as const) {\n\tblockedPublicV6.addSubnet(address, prefix, \"ipv6\");\n}\n\nconst loopbackAddresses = new BlockList();\nloopbackAddresses.addSubnet(\"127.0.0.0\", 8, \"ipv4\");\nloopbackAddresses.addAddress(\"::1\", \"ipv6\");\nloopbackAddresses.addSubnet(\"::ffff:127.0.0.0\", 104, \"ipv6\");\n\n/** True when the literal IP address is a loopback address. Non-IP input is never loopback. */\nexport function isLoopbackAddress(address: string): boolean {\n\tconst family = isIP(address);\n\tif (family === 4) return loopbackAddresses.check(address, \"ipv4\");\n\tif (family === 6) return loopbackAddresses.check(address, \"ipv6\");\n\treturn false;\n}\n\n/** True when the literal IP address must not be dialed for a public origin. Non-IP input fails closed. */\nexport function isBlockedPublicAddress(address: string): boolean {\n\tconst family = isIP(address);\n\tif (family === 4) return blockedPublicV4.check(address, \"ipv4\");\n\tif (family === 6) return blockedPublicV6.check(address, \"ipv6\");\n\treturn true;\n}\n\nexport function getAgentDir(): string {\n\treturn getConfiguredAgentDir();\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isOptionalNonemptyString(value: unknown): value is string | undefined {\n\treturn value === undefined || (typeof value === \"string\" && value.length > 0);\n}\n\nfunction parseModelsConfig(value: unknown): ModelsConfig | undefined {\n\tif (!isRecord(value) || !isRecord(value.providers)) return undefined;\n\tconst providers: Record<string, ProviderConfig> = {};\n\n\tfor (const [providerId, rawProvider] of Object.entries(value.providers)) {\n\t\tif (!isRecord(rawProvider)) return undefined;\n\t\tif (\n\t\t\t!isOptionalNonemptyString(rawProvider.baseUrl) ||\n\t\t\t!isOptionalNonemptyString(rawProvider.apiKey) ||\n\t\t\t!isOptionalNonemptyString(rawProvider.api)\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tlet headers: Record<string, string> | undefined;\n\t\tif (rawProvider.headers !== undefined) {\n\t\t\tif (!isRecord(rawProvider.headers)) return undefined;\n\t\t\theaders = {};\n\t\t\tfor (const [name, headerValue] of Object.entries(rawProvider.headers)) {\n\t\t\t\tif (typeof headerValue !== \"string\") return undefined;\n\t\t\t\theaders[name] = headerValue;\n\t\t\t}\n\t\t}\n\n\t\tlet models: ModelEndpointConfig[] | undefined;\n\t\tif (rawProvider.models !== undefined) {\n\t\t\tif (!Array.isArray(rawProvider.models)) return undefined;\n\t\t\tmodels = [];\n\t\t\tfor (const rawModel of rawProvider.models) {\n\t\t\t\tif (\n\t\t\t\t\t!isRecord(rawModel) ||\n\t\t\t\t\ttypeof rawModel.id !== \"string\" ||\n\t\t\t\t\trawModel.id.length === 0 ||\n\t\t\t\t\t!isOptionalNonemptyString(rawModel.baseUrl) ||\n\t\t\t\t\t!isOptionalNonemptyString(rawModel.api)\n\t\t\t\t) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tmodels.push({ id: rawModel.id, baseUrl: rawModel.baseUrl, api: rawModel.api });\n\t\t\t}\n\t\t}\n\n\t\tproviders[providerId] = {\n\t\t\tbaseUrl: rawProvider.baseUrl,\n\t\t\tapiKey: rawProvider.apiKey,\n\t\t\tapi: rawProvider.api,\n\t\t\theaders,\n\t\t\tmodels,\n\t\t};\n\t}\n\n\treturn { providers };\n}\n\nfunction parseAuthConfig(value: unknown): AuthConfig | undefined {\n\tif (!isRecord(value)) return undefined;\n\tconst auth: AuthConfig = {};\n\tfor (const [providerId, rawEntry] of Object.entries(value)) {\n\t\tif (!isRecord(rawEntry) || (rawEntry.type !== \"api_key\" && rawEntry.type !== \"oauth\")) return undefined;\n\t\tif (rawEntry.type === \"api_key\" && (typeof rawEntry.key !== \"string\" || rawEntry.key.length === 0)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tauth[providerId] = { type: rawEntry.type };\n\t}\n\treturn auth;\n}\n\nconst KIMI_PROVIDER_ID = \"kimi-coding\";\nconst TOML_BASIC_ESCAPES: Record<string, string> = {\n\t'\"': '\"',\n\t\"\\\\\": \"\\\\\",\n\tb: \"\\b\",\n\tf: \"\\f\",\n\tn: \"\\n\",\n\tr: \"\\r\",\n\tt: \"\\t\",\n};\n\ntype TomlQuote = \"basic\" | \"literal\" | \"multiline-basic\" | \"multiline-literal\";\n\ninterface TomlScanState {\n\tquote?: TomlQuote;\n\tcontainers: (\"[\" | \"{\")[];\n}\n\ninterface KimiTomlProviderEntry {\n\tname: string;\n\ttype?: string;\n\tbaseUrl?: string;\n\tmodelName?: string;\n\tapiKey?: string;\n}\n\ninterface KimiConfigValues {\n\tbaseUrl?: string;\n\tmodelName?: string;\n\tproviders: KimiTomlProviderEntry[];\n}\n\ntype KimiConfigLoad =\n\t| { kind: \"missing\" }\n\t| { kind: \"ok\"; value: KimiConfigValues }\n\t| { kind: \"invalid\"; line: number; reason: string };\n\nfunction findBasicStringEnd(text: string, start: number, multiline: boolean): number {\n\tfor (let index = start; index < text.length; index++) {\n\t\tif (text[index] === \"\\\\\") {\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (multiline ? text.startsWith('\"\"\"', index) : text[index] === '\"') return index;\n\t}\n\treturn -1;\n}\n\nfunction decodeTomlBasicString(body: string, multiline: boolean): { value: string } | { error: string } {\n\tlet value = \"\";\n\tfor (let index = 0; index < body.length; index++) {\n\t\tconst char = body[index];\n\t\tif (char !== \"\\\\\") {\n\t\t\tconst code = char.codePointAt(0) ?? 0;\n\t\t\tif (code < 0x20 && char !== \"\\t\" && !(multiline && (char === \"\\n\" || char === \"\\r\"))) {\n\t\t\t\treturn { error: \"invalid control character in string value\" };\n\t\t\t}\n\t\t\tvalue += char;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst escapeChar = body[index + 1];\n\t\tif (escapeChar === undefined) return { error: \"unterminated escape sequence\" };\n\t\tif (multiline && (escapeChar === \"\\n\" || escapeChar === \"\\r\")) {\n\t\t\tindex++;\n\t\t\twhile (index + 1 < body.length && /\\s/.test(body[index + 1])) index++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (escapeChar === \"u\" || escapeChar === \"U\") {\n\t\t\tconst width = escapeChar === \"u\" ? 4 : 8;\n\t\t\tconst hex = body.slice(index + 2, index + 2 + width);\n\t\t\tconst codePoint = /^[0-9a-fA-F]+$/.test(hex) && hex.length === width ? Number.parseInt(hex, 16) : -1;\n\t\t\tif (codePoint < 0 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) {\n\t\t\t\treturn { error: \"invalid unicode escape\" };\n\t\t\t}\n\t\t\tvalue += String.fromCodePoint(codePoint);\n\t\t\tindex += 1 + width;\n\t\t\tcontinue;\n\t\t}\n\t\tconst mapped = TOML_BASIC_ESCAPES[escapeChar];\n\t\tif (mapped === undefined) return { error: \"invalid escape sequence\" };\n\t\tvalue += mapped;\n\t\tindex++;\n\t}\n\treturn { value };\n}\n\n/** Parses one TOML string value. Errors are fixed and never include raw TOML content. */\nfunction parseTomlStringValue(raw: string, requireNonempty = true): { value: string } | { error: string } {\n\tconst text = raw.trim();\n\tconst multilineBasic = text.startsWith('\"\"\"');\n\tconst multilineLiteral = text.startsWith(\"'''\");\n\tconst basic = !multilineBasic && text.startsWith('\"');\n\tconst literal = !multilineLiteral && text.startsWith(\"'\");\n\tif (!multilineBasic && !multilineLiteral && !basic && !literal) {\n\t\treturn { error: \"value must be a quoted string\" };\n\t}\n\n\tconst delimiterWidth = multilineBasic || multilineLiteral ? 3 : 1;\n\tconst end =\n\t\tmultilineBasic || basic\n\t\t\t? findBasicStringEnd(text, delimiterWidth, multilineBasic)\n\t\t\t: text.indexOf(multilineLiteral ? \"'''\" : \"'\", delimiterWidth);\n\tif (end < 0) {\n\t\treturn {\n\t\t\terror:\n\t\t\t\tmultilineBasic || multilineLiteral ? \"unterminated multiline string value\" : \"unterminated string value\",\n\t\t};\n\t}\n\tif (text.slice(end + delimiterWidth).trim().length > 0) {\n\t\treturn { error: \"unexpected content after string value\" };\n\t}\n\n\tlet body = text.slice(delimiterWidth, end);\n\tif ((multilineBasic || multilineLiteral) && body.startsWith(\"\\n\")) body = body.slice(1);\n\tconst parsed =\n\t\tmultilineBasic || basic\n\t\t\t? decodeTomlBasicString(body, multilineBasic)\n\t\t\t: /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/.test(body)\n\t\t\t\t? { error: \"invalid control character in string value\" as const }\n\t\t\t\t: { value: body };\n\tif (\"error\" in parsed) return parsed;\n\treturn requireNonempty && parsed.value.length === 0 ? { error: \"empty string value\" } : parsed;\n}\n\nfunction scanTomlLine(text: string, state: TomlScanState): { content: string; error?: string } {\n\tlet content = \"\";\n\tfor (let index = 0; index < text.length; index++) {\n\t\tconst char = text[index];\n\t\tif (state.quote === \"basic\") {\n\t\t\tcontent += char;\n\t\t\tif (char === \"\\\\\" && index + 1 < text.length) content += text[++index];\n\t\t\telse if (char === '\"') state.quote = undefined;\n\t\t\tcontinue;\n\t\t}\n\t\tif (state.quote === \"literal\") {\n\t\t\tcontent += char;\n\t\t\tif (char === \"'\") state.quote = undefined;\n\t\t\tcontinue;\n\t\t}\n\t\tif (state.quote === \"multiline-basic\") {\n\t\t\tif (text.startsWith('\"\"\"', index)) {\n\t\t\t\tcontent += '\"\"\"';\n\t\t\t\tindex += 2;\n\t\t\t\tstate.quote = undefined;\n\t\t\t} else {\n\t\t\t\tcontent += char;\n\t\t\t\tif (char === \"\\\\\" && index + 1 < text.length) content += text[++index];\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (state.quote === \"multiline-literal\") {\n\t\t\tif (text.startsWith(\"'''\", index)) {\n\t\t\t\tcontent += \"'''\";\n\t\t\t\tindex += 2;\n\t\t\t\tstate.quote = undefined;\n\t\t\t} else {\n\t\t\t\tcontent += char;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"#\") break;\n\t\tif (text.startsWith('\"\"\"', index)) {\n\t\t\tcontent += '\"\"\"';\n\t\t\tindex += 2;\n\t\t\tstate.quote = \"multiline-basic\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (text.startsWith(\"'''\", index)) {\n\t\t\tcontent += \"'''\";\n\t\t\tindex += 2;\n\t\t\tstate.quote = \"multiline-literal\";\n\t\t\tcontinue;\n\t\t}\n\t\tcontent += char;\n\t\tif (char === '\"') state.quote = \"basic\";\n\t\telse if (char === \"'\") state.quote = \"literal\";\n\t\telse if (char === \"[\" || char === \"{\") state.containers.push(char);\n\t\telse if (char === \"]\" || char === \"}\") {\n\t\t\tconst expected = char === \"]\" ? \"[\" : \"{\";\n\t\t\tif (state.containers.pop() !== expected) return { content, error: \"mismatched value delimiter\" };\n\t\t}\n\t}\n\tif (state.quote === \"basic\" || state.quote === \"literal\") {\n\t\treturn { content, error: \"unterminated string value\" };\n\t}\n\treturn { content };\n}\n\nfunction consumeTomlValue(\n\tlines: readonly string[],\n\tstartLine: number,\n\tinitial: string,\n): { value?: string; endLine: number; error?: string } {\n\tconst state: TomlScanState = { containers: [] };\n\tconst chunks: string[] = [];\n\tfor (let line = startLine; line < lines.length; line++) {\n\t\tconst scanned = scanTomlLine(line === startLine ? initial : lines[line], state);\n\t\tchunks.push(scanned.content);\n\t\tif (scanned.error) return { endLine: line, error: scanned.error };\n\t\tif (!state.quote && state.containers.length === 0) {\n\t\t\tconst value = chunks.join(\"\\n\").trim();\n\t\t\treturn value.length > 0\n\t\t\t\t? { value, endLine: line }\n\t\t\t\t: { endLine: startLine, error: \"assignment value is missing\" };\n\t\t}\n\t}\n\treturn {\n\t\tendLine: startLine,\n\t\terror: state.quote ? \"unterminated multiline string value\" : \"unterminated compound value\",\n\t};\n}\n\nfunction findTomlTopLevelCharacter(text: string, wanted: string): number {\n\tconst state: TomlScanState = { containers: [] };\n\tfor (let index = 0; index < text.length; index++) {\n\t\tconst char = text[index];\n\t\tif (state.quote === \"basic\" || state.quote === \"multiline-basic\") {\n\t\t\tif (char === \"\\\\\") index++;\n\t\t\telse if (state.quote === \"basic\" && char === '\"') state.quote = undefined;\n\t\t\telse if (state.quote === \"multiline-basic\" && text.startsWith('\"\"\"', index)) {\n\t\t\t\tstate.quote = undefined;\n\t\t\t\tindex += 2;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (state.quote === \"literal\" || state.quote === \"multiline-literal\") {\n\t\t\tconst delimiter = state.quote === \"literal\" ? \"'\" : \"'''\";\n\t\t\tif (text.startsWith(delimiter, index)) {\n\t\t\t\tstate.quote = undefined;\n\t\t\t\tindex += delimiter.length - 1;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"#\") return -1;\n\t\tif (text.startsWith('\"\"\"', index)) {\n\t\t\tstate.quote = \"multiline-basic\";\n\t\t\tindex += 2;\n\t\t} else if (text.startsWith(\"'''\", index)) {\n\t\t\tstate.quote = \"multiline-literal\";\n\t\t\tindex += 2;\n\t\t} else if (char === '\"') state.quote = \"basic\";\n\t\telse if (char === \"'\") state.quote = \"literal\";\n\t\telse if (char === \"[\" || char === \"{\") state.containers.push(char);\n\t\telse if (char === \"]\" || char === \"}\") state.containers.pop();\n\t\telse if (char === wanted && state.containers.length === 0) return index;\n\t}\n\treturn -1;\n}\n\nfunction parseTomlDottedKey(raw: string): string[] | undefined {\n\tconst components: string[] = [];\n\tlet index = 0;\n\twhile (index < raw.length) {\n\t\twhile (/\\s/.test(raw[index] ?? \"\")) index++;\n\t\tlet component: string | undefined;\n\t\tif (raw[index] === '\"') {\n\t\t\tconst end = findBasicStringEnd(raw, index + 1, false);\n\t\t\tif (end < 0) return undefined;\n\t\t\tconst parsed = parseTomlStringValue(raw.slice(index, end + 1), false);\n\t\t\tif (\"error\" in parsed) return undefined;\n\t\t\tcomponent = parsed.value;\n\t\t\tindex = end + 1;\n\t\t} else if (raw[index] === \"'\") {\n\t\t\tconst end = raw.indexOf(\"'\", index + 1);\n\t\t\tif (end < 0) return undefined;\n\t\t\tcomponent = raw.slice(index + 1, end);\n\t\t\tindex = end + 1;\n\t\t} else {\n\t\t\tconst match = /^[A-Za-z0-9_-]+/.exec(raw.slice(index));\n\t\t\tif (!match) return undefined;\n\t\t\tcomponent = match[0];\n\t\t\tindex += match[0].length;\n\t\t}\n\t\tcomponents.push(component);\n\t\twhile (/\\s/.test(raw[index] ?? \"\")) index++;\n\t\tif (index === raw.length) return components;\n\t\tif (raw[index] !== \".\") return undefined;\n\t\tindex++;\n\t\tif (index === raw.length) return undefined;\n\t}\n\treturn undefined;\n}\n\nfunction splitTomlTopLevel(raw: string): string[] {\n\tconst parts: string[] = [];\n\tlet start = 0;\n\tfor (let index = 0; index < raw.length; index++) {\n\t\tif (raw[index] !== \",\") continue;\n\t\tconst relative = findTomlTopLevelCharacter(raw.slice(start, index + 1), \",\");\n\t\tif (relative !== index - start) continue;\n\t\tparts.push(raw.slice(start, index).trim());\n\t\tstart = index + 1;\n\t}\n\tparts.push(raw.slice(start).trim());\n\treturn parts;\n}\n\nfunction isValidTomlScalar(raw: string): boolean {\n\treturn (\n\t\traw === \"true\" ||\n\t\traw === \"false\" ||\n\t\t/^[+-]?(?:0|[1-9](?:_?\\d)*)$/.test(raw) ||\n\t\t/^[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*$/.test(raw) ||\n\t\t/^[+-]?0o[0-7](?:_?[0-7])*$/.test(raw) ||\n\t\t/^[+-]?0b[01](?:_?[01])*$/.test(raw) ||\n\t\t/^[+-]?(?:(?:0|[1-9](?:_?\\d)*)\\.(?:\\d(?:_?\\d)*)(?:[eE][+-]?\\d(?:_?\\d)*)?|(?:0|[1-9](?:_?\\d)*)[eE][+-]?\\d(?:_?\\d)*|inf|nan)$/.test(\n\t\t\traw,\n\t\t) ||\n\t\t/^\\d{4}-\\d{2}-\\d{2}(?:[Tt ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})?)?$/.test(raw) ||\n\t\t/^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?$/.test(raw)\n\t);\n}\n\nfunction isValidTomlValue(raw: string): boolean {\n\tconst text = raw.trim();\n\tif (text.startsWith('\"') || text.startsWith(\"'\")) return !(\"error\" in parseTomlStringValue(text, false));\n\tif (text.startsWith(\"[\") && text.endsWith(\"]\")) {\n\t\tconst parts = splitTomlTopLevel(text.slice(1, -1));\n\t\tif (parts.at(-1) === \"\") parts.pop();\n\t\treturn parts.every((part) => part.length > 0 && isValidTomlValue(part));\n\t}\n\tif (text.startsWith(\"{\") && text.endsWith(\"}\")) {\n\t\tconst body = text.slice(1, -1).trim();\n\t\tif (body.length === 0) return true;\n\t\treturn splitTomlTopLevel(body).every((part) => {\n\t\t\tconst equals = findTomlTopLevelCharacter(part, \"=\");\n\t\t\treturn equals > 0 && !!parseTomlDottedKey(part.slice(0, equals)) && isValidTomlValue(part.slice(equals + 1));\n\t\t});\n\t}\n\treturn isValidTomlScalar(text);\n}\n\nfunction parseTomlTable(line: string): { components?: string[]; array?: boolean; error?: string } {\n\tconst state: TomlScanState = { containers: [] };\n\tconst scanned = scanTomlLine(line, state);\n\tif (scanned.error || state.quote || state.containers.length > 0) return { error: \"malformed table header\" };\n\tconst text = scanned.content.trim();\n\tconst array = text.startsWith(\"[[\");\n\tif (array ? !text.endsWith(\"]]\") : !text.endsWith(\"]\")) return { error: \"malformed table header\" };\n\tconst inner = text.slice(array ? 2 : 1, array ? -2 : -1).trim();\n\tconst components = parseTomlDottedKey(inner);\n\treturn components && components.length > 0 ? { components, array } : { error: \"malformed table header\" };\n}\n\n/**\n * Strict Kimi TOML subset reader. It validates every table/assignment and generic TOML value,\n * but extracts only root `base_url`/`model_name` and `[providers.<name>]` string fields. Fixed\n * line-number errors ensure credentials and other file content are never echoed.\n */\nfunction loadKimiConfig(path: string): KimiConfigLoad {\n\tif (!existsSync(path)) return { kind: \"missing\" };\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(path, \"utf-8\");\n\t} catch {\n\t\treturn { kind: \"invalid\", line: 0, reason: \"file is unreadable\" };\n\t}\n\tconst value: KimiConfigValues = { providers: [] };\n\tconst lines = content.split(/\\r?\\n/);\n\tconst assignments = new Set<string>();\n\tlet table: KimiTomlProviderEntry | \"root\" | \"other\" = \"root\";\n\tlet scope = \"root\";\n\tfor (let index = 0; index < lines.length; index++) {\n\t\tconst line = lines[index].trim();\n\t\tif (line.length === 0 || line.startsWith(\"#\")) continue;\n\t\tif (line.startsWith(\"[\")) {\n\t\t\tconst parsed = parseTomlTable(line);\n\t\t\tif (parsed.error || !parsed.components) {\n\t\t\t\treturn { kind: \"invalid\", line: index + 1, reason: parsed.error ?? \"malformed table header\" };\n\t\t\t}\n\t\t\tconst [first, name] = parsed.components;\n\t\t\tif (first === \"providers\" && parsed.array) {\n\t\t\t\treturn { kind: \"invalid\", line: index + 1, reason: \"provider table cannot be an array\" };\n\t\t\t}\n\t\t\tif (first === \"providers\" && parsed.components.length === 2 && name !== undefined && name.length > 0) {\n\t\t\t\tconst entry: KimiTomlProviderEntry = { name };\n\t\t\t\tvalue.providers.push(entry);\n\t\t\t\ttable = entry;\n\t\t\t} else {\n\t\t\t\ttable = \"other\";\n\t\t\t}\n\t\t\tscope = `${parsed.array ? \"array\" : \"table\"}:${JSON.stringify(parsed.components)}`;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst assignmentLine = index;\n\t\tconst equals = findTomlTopLevelCharacter(line, \"=\");\n\t\tconst key = equals > 0 ? parseTomlDottedKey(line.slice(0, equals)) : undefined;\n\t\tif (!key || key.length === 0) return { kind: \"invalid\", line: index + 1, reason: \"invalid assignment\" };\n\t\tconst consumed = consumeTomlValue(lines, index, line.slice(equals + 1));\n\t\tif (consumed.error || consumed.value === undefined) {\n\t\t\treturn { kind: \"invalid\", line: assignmentLine + 1, reason: consumed.error ?? \"invalid assignment value\" };\n\t\t}\n\t\tindex = consumed.endLine;\n\t\tif (!isValidTomlValue(consumed.value)) {\n\t\t\treturn { kind: \"invalid\", line: assignmentLine + 1, reason: \"invalid assignment value\" };\n\t\t}\n\t\tconst assignmentId = `${scope}:${JSON.stringify(key)}`;\n\t\tif (assignments.has(assignmentId)) {\n\t\t\treturn { kind: \"invalid\", line: assignmentLine + 1, reason: \"duplicate assignment\" };\n\t\t}\n\t\tassignments.add(assignmentId);\n\t\tif (key.length !== 1 || table === \"other\") continue;\n\n\t\tconst keyName = key[0];\n\t\tconst relevant =\n\t\t\ttable === \"root\"\n\t\t\t\t? keyName === \"base_url\" || keyName === \"model_name\"\n\t\t\t\t: keyName === \"base_url\" ||\n\t\t\t\t\tkeyName === \"model_name\" ||\n\t\t\t\t\tkeyName === \"model\" ||\n\t\t\t\t\tkeyName === \"type\" ||\n\t\t\t\t\tkeyName === \"api_key\";\n\t\tif (!relevant) continue;\n\t\tconst parsedValue = parseTomlStringValue(consumed.value);\n\t\tif (\"error\" in parsedValue) return { kind: \"invalid\", line: assignmentLine + 1, reason: parsedValue.error };\n\t\tif (table === \"root\") {\n\t\t\tif (keyName === \"base_url\") value.baseUrl = parsedValue.value;\n\t\t\telse value.modelName = parsedValue.value;\n\t\t} else {\n\t\t\tif (keyName === \"base_url\") table.baseUrl = parsedValue.value;\n\t\t\telse if (keyName === \"type\") table.type = parsedValue.value;\n\t\t\telse if (keyName === \"api_key\") table.apiKey = parsedValue.value;\n\t\t\telse table.modelName = parsedValue.value;\n\t\t}\n\t}\n\treturn { kind: \"ok\", value };\n}\n\nfunction nonemptyEnv(name: string): string | undefined {\n\tconst value = process.env[name];\n\treturn value && value.length > 0 ? value : undefined;\n}\n\ninterface KimiResolution {\n\tbaseUrl?: string;\n\tmodelName?: string;\n\tbaseUrlSource?: \"kimi-environment\" | \"kimi-config-toml\";\n\t/** First `[providers.<name>]` entry with `type = \"openai_legacy\"` and a base URL (§10.2). */\n\tcustomProvider?: { name: string; baseUrl: string; modelName?: string; apiKey?: string };\n\tfailure?: { line: number; reason: string };\n}\n\nfunction resolveKimiConfig(kimiConfigPath: string | undefined): KimiResolution {\n\tconst path = kimiConfigPath ?? join(homedir(), \".kimi\", \"config.toml\");\n\tconst loaded = loadKimiConfig(path);\n\tif (loaded.kind === \"invalid\") return { failure: { line: loaded.line, reason: loaded.reason } };\n\tconst fileValues: KimiConfigValues = loaded.kind === \"ok\" ? loaded.value : { providers: [] };\n\tconst envBaseUrl = nonemptyEnv(\"KIMI_BASE_URL\");\n\tconst envModelName = nonemptyEnv(\"KIMI_MODEL_NAME\");\n\tconst custom = fileValues.providers.find((entry) => entry.type === \"openai_legacy\" && entry.baseUrl);\n\treturn {\n\t\tbaseUrl: envBaseUrl ?? fileValues.baseUrl,\n\t\tmodelName: envModelName ?? fileValues.modelName,\n\t\tbaseUrlSource: envBaseUrl ? \"kimi-environment\" : fileValues.baseUrl ? \"kimi-config-toml\" : undefined,\n\t\tcustomProvider: custom\n\t\t\t? { name: custom.name, baseUrl: custom.baseUrl as string, modelName: custom.modelName, apiKey: custom.apiKey }\n\t\t\t: undefined,\n\t};\n}\n\nfunction loadConfig<T>(path: string, parse: (value: unknown) => T | undefined, allowComments = false): LoadedConfig<T> {\n\tif (!existsSync(path)) return { kind: \"missing\" };\n\ttry {\n\t\tconst content = readFileSync(path, \"utf-8\");\n\t\tconst parsed = JSON.parse(allowComments ? stripJsonComments(content) : content) as unknown;\n\t\tconst value = parse(parsed);\n\t\treturn value ? { kind: \"ok\", value } : { kind: \"invalid\" };\n\t} catch {\n\t\treturn { kind: \"invalid\" };\n\t}\n}\n\nexport function normalizedUrlHostname(url: URL): string {\n\treturn normalizedHostname(url);\n}\n\nfunction normalizedHostname(url: URL): string {\n\tconst hostname = url.hostname.toLowerCase();\n\tconst unwrapped = hostname.startsWith(\"[\") && hostname.endsWith(\"]\") ? hostname.slice(1, -1) : hostname;\n\treturn unwrapped.endsWith(\".\") ? unwrapped.slice(0, -1) : unwrapped;\n}\n\nfunction isLiteralLoopback(hostname: string): boolean {\n\tif (hostname === \"localhost\" || hostname === \"::1\") return true;\n\tif (isIP(hostname) !== 4) return false;\n\treturn Number(hostname.split(\".\")[0]) === 127;\n}\n\nfunction isBlockedPublicHost(hostname: string): boolean {\n\tif (hostname === \"localhost\" || hostname.endsWith(\".localhost\")) return true;\n\tif (isIP(hostname) === 0) return false;\n\treturn isBlockedPublicAddress(hostname);\n}\n\nfunction sanitizeUrl(rawUrl: string): URL | undefined {\n\ttry {\n\t\tconst url = new URL(rawUrl);\n\t\turl.username = \"\";\n\t\turl.password = \"\";\n\t\turl.search = \"\";\n\t\turl.hash = \"\";\n\t\treturn url;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function canonicalProviderEndpointUrl(rawUrl: string | undefined): string | undefined {\n\tif (!rawUrl) return undefined;\n\tconst url = sanitizeUrl(rawUrl);\n\tif (!url) return undefined;\n\tconst hostname = normalizedHostname(url);\n\tconst host = isIP(hostname) === 6 ? `[${hostname}]` : hostname;\n\tconst pathname = url.pathname.replace(/\\/+$/, \"\") || \"/\";\n\treturn `${url.protocol}//${host}${url.port ? `:${url.port}` : \"\"}${pathname}`;\n}\n\nfunction credentialBindingMatchesTarget(\n\tbinding: ProviderDoctorCredentialBinding,\n\ttarget: ResolvedProviderTarget,\n): boolean {\n\treturn (\n\t\tbinding.origin === target.origin &&\n\t\tbinding.source === target.source &&\n\t\tbinding.baseUrl === canonicalProviderEndpointUrl(target.endpoint.baseUrl) &&\n\t\tbinding.api === target.endpoint.api &&\n\t\tbinding.modelId === target.endpoint.modelId\n\t);\n}\n\nfunction appendPath(url: URL, segment: string): URL {\n\tconst next = URL.parse(url.href);\n\tif (!next) throw new Error(\"validated provider URL could not be cloned\");\n\tnext.pathname = `${next.pathname.replace(/\\/$/, \"\")}/${segment}`;\n\treturn next;\n}\n\nfunction makeRootProbeUrl(url: URL, origin: ProviderOrigin): URL {\n\tconst root = URL.parse(url.href);\n\tif (!root) throw new Error(\"validated provider URL could not be cloned\");\n\tif (origin !== \"local-proxy\") return root;\n\troot.pathname = \"/health\";\n\treturn root;\n}\n\nfunction publicResult(\n\ttarget: ResolvedProviderTarget,\n\tlevel: ProviderDoctorLevel,\n\tchecks: ProviderDoctorCheck[],\n\turl: URL | undefined,\n\tfailure?: ProbeFailure,\n\tcostWarning?: boolean,\n): ProviderDoctorResult {\n\tconst targetUrl = url?.href;\n\treturn {\n\t\tprovider: target.providerId,\n\t\tstatus: failure ? \"fail\" : \"ok\",\n\t\tlevel,\n\t\torigin: target.origin,\n\t\tsource: target.source,\n\t\ttargetUrl,\n\t\tbaseUrl: targetUrl,\n\t\tapi: target.endpoint.api,\n\t\tmodelId: target.endpoint.modelId,\n\t\tauthPresent: target.auth.present,\n\t\t...(costWarning ? { costWarning: true } : {}),\n\t\tchecks,\n\t\terror: failure,\n\t};\n}\n\nfunction unresolvedFailure(\n\tproviderId: string,\n\tlevel: ProviderDoctorLevel,\n\tcode: ProviderDoctorCode,\n\tmessage: string,\n): ProviderDoctorResult {\n\tconst check: ProviderDoctorCheck = { name: \"config\", status: \"fail\", category: \"config\", code, message };\n\treturn {\n\t\tprovider: providerId,\n\t\tstatus: \"fail\",\n\t\tlevel,\n\t\torigin: \"unknown\",\n\t\tsource: \"unresolved\",\n\t\tauthPresent: false,\n\t\tchecks: [check],\n\t\terror: { category: \"config\", code, message },\n\t};\n}\n\nfunction staticChecks(target: ResolvedProviderTarget): {\n\tchecks: ProviderDoctorCheck[];\n\turl?: URL;\n\tfailure?: ProbeFailure;\n} {\n\tconst checks: ProviderDoctorCheck[] = [\n\t\t{ name: \"config-present\", status: \"ok\", code: \"ok\", message: \"Provider configuration resolved\" },\n\t];\n\tif (target.origin === \"custom-openai-compatible\") {\n\t\tchecks.push({\n\t\t\tname: \"native-provider-checks\",\n\t\t\tstatus: \"skipped\",\n\t\t\tcode: \"ok\",\n\t\t\tmessage: \"Custom OpenAI-compatible endpoint; native-only provider checks skipped\",\n\t\t});\n\t}\n\tlet failure: ProbeFailure | undefined;\n\tconst recordFailure = (next: ProbeFailure): void => {\n\t\tfailure ??= next;\n\t};\n\n\tconst requestedModel = target.endpoint.modelId;\n\tconst modelIds = target.endpoint.modelIds ?? [];\n\tif (!requestedModel) {\n\t\tchecks.push({ name: \"model-provider-relation\", status: \"skipped\", code: \"ok\", message: \"No model selected\" });\n\t} else if (!modelIds.includes(requestedModel)) {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"model\",\n\t\t\tcode: \"model-provider-mismatch\",\n\t\t\tmessage: \"Selected model does not belong to the provider\",\n\t\t};\n\t\tchecks.push({ name: \"model-provider-relation\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else {\n\t\tchecks.push({ name: \"model-provider-relation\", status: \"ok\", code: \"ok\", message: \"Model belongs to provider\" });\n\t}\n\n\tconst api = target.endpoint.api;\n\tif (!api) {\n\t\tconst next: ProbeFailure = { category: \"config\", code: \"api-missing\", message: \"Provider API type is missing\" };\n\t\tchecks.push({ name: \"api\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else if (\n\t\t!KNOWN_APIS.has(api) ||\n\t\t(target.origin === \"custom-openai-compatible\" && !OPENAI_COMPATIBLE_APIS.has(api))\n\t) {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"api-unsupported\",\n\t\t\tmessage: \"Provider API type is unsupported\",\n\t\t};\n\t\tchecks.push({ name: \"api\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else {\n\t\tchecks.push({ name: \"api\", status: \"ok\", code: \"ok\", message: \"Provider API type is supported\" });\n\t}\n\n\tlet url: URL | undefined;\n\tif (!target.endpoint.baseUrl) {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"base-url-missing\",\n\t\t\tmessage: \"Provider base URL is missing\",\n\t\t};\n\t\tchecks.push({ name: \"base-url\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else {\n\t\turl = sanitizeUrl(target.endpoint.baseUrl);\n\t\tif (!url) {\n\t\t\tconst next: ProbeFailure = {\n\t\t\t\tcategory: \"config\",\n\t\t\t\tcode: \"base-url-invalid\",\n\t\t\t\tmessage: \"Provider base URL is invalid\",\n\t\t\t};\n\t\t\tchecks.push({ name: \"base-url\", status: \"fail\", ...next });\n\t\t\trecordFailure(next);\n\t\t} else {\n\t\t\tconst allowedScheme =\n\t\t\t\ttarget.origin === \"local-proxy\"\n\t\t\t\t\t? url.protocol === \"http:\" || url.protocol === \"https:\"\n\t\t\t\t\t: url.protocol === \"https:\";\n\t\t\tif (!allowedScheme) {\n\t\t\t\tconst next: ProbeFailure = {\n\t\t\t\t\tcategory: \"config\",\n\t\t\t\t\tcode: \"url-scheme-unsupported\",\n\t\t\t\t\tmessage: \"Provider URL scheme is not allowed\",\n\t\t\t\t};\n\t\t\t\tchecks.push({ name: \"base-url\", status: \"fail\", ...next });\n\t\t\t\trecordFailure(next);\n\t\t\t} else {\n\t\t\t\tchecks.push({ name: \"base-url\", status: \"ok\", code: \"ok\", message: \"Provider base URL is valid\" });\n\t\t\t}\n\t\t}\n\t}\n\n\tif (target.origin === \"unknown\") {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"origin-unsupported\",\n\t\t\tmessage: \"Provider origin is unsupported\",\n\t\t};\n\t\tchecks.push({ name: \"origin-policy\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else if (url) {\n\t\tconst hostname = normalizedHostname(url);\n\t\tconst blocked = target.origin === \"local-proxy\" ? !isLiteralLoopback(hostname) : isBlockedPublicHost(hostname);\n\t\tif (blocked) {\n\t\t\tconst next: ProbeFailure = {\n\t\t\t\tcategory: \"config\",\n\t\t\t\tcode: \"address-policy-blocked\",\n\t\t\t\tmessage: \"Provider address is blocked by origin policy\",\n\t\t\t};\n\t\t\tchecks.push({ name: \"origin-policy\", status: \"fail\", ...next });\n\t\t\trecordFailure(next);\n\t\t} else {\n\t\t\tchecks.push({ name: \"origin-policy\", status: \"ok\", code: \"ok\", message: \"Provider origin policy passed\" });\n\t\t}\n\t} else {\n\t\tchecks.push({ name: \"origin-policy\", status: \"skipped\", code: \"ok\", message: \"No valid URL to inspect\" });\n\t}\n\n\tif (target.auth.present) {\n\t\tchecks.push({ name: \"auth-present\", status: \"ok\", code: \"ok\", message: \"Authentication is configured\" });\n\t} else if (target.origin === \"custom-openai-compatible\" || target.origin === \"local-proxy\") {\n\t\t// Native login checks do not apply: custom/local OpenAI-compatible endpoints may be keyless.\n\t\tchecks.push({\n\t\t\tname: \"auth-present\",\n\t\t\tstatus: \"skipped\",\n\t\t\tcode: \"ok\",\n\t\t\tmessage: \"No credential configured; custom endpoints may be keyless\",\n\t\t});\n\t} else {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"auth\",\n\t\t\tcode: \"auth-missing\",\n\t\t\tmessage: \"Authentication is not configured\",\n\t\t};\n\t\tchecks.push({ name: \"auth-present\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t}\n\n\treturn { checks, url, failure };\n}\n\ntype ProbeEndpointKind = \"root\" | \"models\" | \"model-probe\";\n\nfunction classifyProbe(name: string, kind: ProbeEndpointKind, status: number): ProbeOutcome {\n\tconst forModels = kind === \"models\";\n\tif (Number.isInteger(status) && status >= 200 && status < 300) {\n\t\tconst probe: EndpointProbeResult = { reachable: true, authenticated: true, status, category: \"ok\" };\n\t\tif (forModels) probe.modelsSupported = true;\n\t\treturn { check: { name, status: \"ok\", code: \"ok\", message: `HTTP ${status}`, probe } };\n\t}\n\tif (status >= 300 && status < 400) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"redirect-blocked\",\n\t\t\tmessage: \"Provider probe returned a blocked redirect\",\n\t\t};\n\t\treturn { check: { name, status: \"fail\", ...failure }, failure };\n\t}\n\tif (status === 401 || status === 403) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"auth\",\n\t\t\tcode: \"authentication-failed\",\n\t\t\tmessage: \"Provider rejected authentication\",\n\t\t};\n\t\tconst probe: EndpointProbeResult = { reachable: true, authenticated: false, status, category: \"auth\" };\n\t\treturn { check: { name, status: \"fail\", ...failure, probe }, failure };\n\t}\n\tif (status === 404 || status === 405 || status === 501) {\n\t\t// A root or /models 404 is neutral: OpenAI-compatible servers may not expose these paths.\n\t\tconst probe: EndpointProbeResult = { reachable: true, status, category: \"unsupported-endpoint\" };\n\t\tif (forModels) probe.modelsSupported = false;\n\t\treturn {\n\t\t\tcheck: {\n\t\t\t\tname,\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t\tcode: \"endpoint-unsupported\",\n\t\t\t\tmessage: `HTTP ${status}; endpoint is not required`,\n\t\t\t\tprobe,\n\t\t\t},\n\t\t};\n\t}\n\tif (status >= 500 && status <= 599) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"server\",\n\t\t\tcode: \"server-error\",\n\t\t\tmessage: \"Provider endpoint returned a server error\",\n\t\t};\n\t\tconst probe: EndpointProbeResult = { reachable: true, status, category: \"server\" };\n\t\treturn { check: { name, status: \"fail\", ...failure, probe }, failure };\n\t}\n\tif (status === 408 || status === 429) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"network\",\n\t\t\tcode: \"network-failure\",\n\t\t\tmessage: \"Provider probe failed at the network boundary\",\n\t\t};\n\t\tconst probe: EndpointProbeResult = { reachable: true, status, category: \"network\" };\n\t\treturn { check: { name, status: \"fail\", ...failure, probe }, failure };\n\t}\n\n\tconst failure: ProbeFailure = {\n\t\tcategory: \"config\",\n\t\tcode: \"unexpected-response\",\n\t\tmessage: \"Provider probe returned an unexpected response\",\n\t};\n\treturn { check: { name, status: \"fail\", ...failure }, failure };\n}\n\nfunction createComposedController(\n\tsignal: AbortSignal | undefined,\n\ttimeoutMs: number,\n): {\n\tcontroller: AbortController;\n\tabortKind: () => \"aborted\" | \"timeout\" | undefined;\n\tdispose: () => void;\n} {\n\tconst controller = new AbortController();\n\tlet kind: \"aborted\" | \"timeout\" | undefined;\n\tconst abortFromCaller = (): void => {\n\t\tkind ??= \"aborted\";\n\t\tcontroller.abort();\n\t};\n\tif (signal?.aborted) abortFromCaller();\n\telse signal?.addEventListener(\"abort\", abortFromCaller, { once: true });\n\tconst timeout = setTimeout(() => {\n\t\tkind ??= \"timeout\";\n\t\tcontroller.abort();\n\t}, timeoutMs);\n\n\treturn {\n\t\tcontroller,\n\t\tabortKind: () => kind,\n\t\tdispose: () => {\n\t\t\tclearTimeout(timeout);\n\t\t\tsignal?.removeEventListener(\"abort\", abortFromCaller);\n\t\t},\n\t};\n}\n\nfunction requestWithAbort(\n\ttransport: ProviderDoctorTransport,\n\trequest: ProviderDoctorTransportRequest,\n): Promise<ProviderDoctorTransportResponse> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (request.signal.aborted) {\n\t\t\treject(ABORTED);\n\t\t\treturn;\n\t\t}\n\t\tconst onAbort = (): void => reject(ABORTED);\n\t\trequest.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\tlet pending: Promise<ProviderDoctorTransportResponse>;\n\t\ttry {\n\t\t\tpending = transport.request(request);\n\t\t} catch (error) {\n\t\t\trequest.signal.removeEventListener(\"abort\", onAbort);\n\t\t\treject(error instanceof ProviderDoctorAuthMaterializationError ? error : undefined);\n\t\t\treturn;\n\t\t}\n\t\tvoid pending.then(\n\t\t\t(response) => {\n\t\t\t\trequest.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\tresolve(response);\n\t\t\t},\n\t\t\t(error: unknown) => {\n\t\t\t\trequest.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\treject(error instanceof ProviderDoctorAuthMaterializationError ? error : undefined);\n\t\t\t},\n\t\t);\n\t});\n}\n\ninterface ProbePayload {\n\tbody: string;\n\tcontentType: \"application/json\";\n}\n\nasync function runProbe(\n\tname: string,\n\tkind: ProbeEndpointKind,\n\turl: URL,\n\ttransport: ProviderDoctorTransport,\n\taddressPolicy: ProviderDoctorAddressPolicy,\n\tcreateHeaders: ProviderDoctorHeaderFactory,\n\tcontroller: ReturnType<typeof createComposedController>,\n\tpayload?: ProbePayload,\n): Promise<ProbeOutcome> {\n\ttry {\n\t\tconst response = await requestWithAbort(transport, {\n\t\t\turl,\n\t\t\tmethod: payload ? \"POST\" : \"GET\",\n\t\t\tredirect: \"manual\",\n\t\t\tsignal: controller.controller.signal,\n\t\t\taddressPolicy,\n\t\t\tcreateHeaders,\n\t\t\tbody: payload?.body,\n\t\t\tcontentType: payload?.contentType,\n\t\t});\n\t\treturn classifyProbe(name, kind, response.status);\n\t} catch (error) {\n\t\tif (error instanceof ProviderDoctorAuthMaterializationError) {\n\t\t\tconst failure: ProbeFailure = {\n\t\t\t\tcategory: \"auth\",\n\t\t\t\tcode: \"auth-materialization-failed\",\n\t\t\t\tmessage: \"Configured authentication could not be materialized; verify auth and config references\",\n\t\t\t};\n\t\t\treturn {\n\t\t\t\tcheck: {\n\t\t\t\t\tname,\n\t\t\t\t\tstatus: \"fail\",\n\t\t\t\t\t...failure,\n\t\t\t\t\tprobe: { reachable: false, authenticated: false, category: \"auth\" },\n\t\t\t\t},\n\t\t\t\tfailure,\n\t\t\t};\n\t\t}\n\t\tconst abortKind = controller.abortKind();\n\t\tconst failure: ProbeFailure = abortKind\n\t\t\t? {\n\t\t\t\t\tcategory: \"network\",\n\t\t\t\t\tcode: abortKind === \"timeout\" ? \"request-timeout\" : \"request-aborted\",\n\t\t\t\t\tmessage: abortKind === \"timeout\" ? \"Provider probe timed out\" : \"Provider probe was aborted\",\n\t\t\t\t}\n\t\t\t: { category: \"network\", code: \"network-failure\", message: \"Provider transport failed\" };\n\t\treturn {\n\t\t\tcheck: { name, status: \"fail\", ...failure, probe: { reachable: false, category: \"network\" } },\n\t\t\tfailure,\n\t\t};\n\t}\n}\n\n/** Minimal-token generative probe payload; never includes tools (no tool-call probe). */\nfunction modelProbeSpec(api: string, modelId: string): { path: string; payload: ProbePayload } | undefined {\n\tif (api === \"openai-completions\") {\n\t\treturn {\n\t\t\tpath: \"chat/completions\",\n\t\t\tpayload: {\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tmodel: modelId,\n\t\t\t\t\tmessages: [{ role: \"user\", content: \"ping\" }],\n\t\t\t\t\tmax_tokens: 1,\n\t\t\t\t\tstream: false,\n\t\t\t\t}),\n\t\t\t\tcontentType: \"application/json\",\n\t\t\t},\n\t\t};\n\t}\n\tif (api === \"openai-responses\") {\n\t\treturn {\n\t\t\tpath: \"responses\",\n\t\t\tpayload: {\n\t\t\t\t// The responses API rejects max_output_tokens below 16; still the minimal request.\n\t\t\t\tbody: JSON.stringify({ model: modelId, input: \"ping\", max_output_tokens: 16, stream: false }),\n\t\t\t\tcontentType: \"application/json\",\n\t\t\t},\n\t\t};\n\t}\n\treturn undefined;\n}\n\nfunction classifyOrigin(baseUrl: string | undefined, api: string | undefined, isNative: boolean): ProviderOrigin {\n\tconst url = baseUrl ? sanitizeUrl(baseUrl) : undefined;\n\tif (url && isLiteralLoopback(normalizedHostname(url))) {\n\t\treturn \"local-proxy\";\n\t}\n\tif (isNative) return \"native\";\n\tif (api && OPENAI_COMPATIBLE_APIS.has(api)) return \"custom-openai-compatible\";\n\treturn \"unknown\";\n}\n\nfunction resolveProviderTarget(\n\tproviderId: string,\n\toptions: ProviderDoctorOptions,\n): { target?: ResolvedProviderTarget; failure?: ProviderDoctorResult } {\n\tconst level = options.level ?? 0;\n\tconst agentDir = options.agentDir ?? getConfiguredAgentDir();\n\tconst modelsConfig = loadConfig(join(agentDir, \"models.json\"), parseModelsConfig, true);\n\tif (modelsConfig.kind === \"invalid\") {\n\t\treturn {\n\t\t\tfailure: unresolvedFailure(providerId, level, \"models-config-invalid\", \"models.json is invalid\"),\n\t\t};\n\t}\n\tconst authConfig = loadConfig(join(agentDir, \"auth.json\"), parseAuthConfig);\n\tif (authConfig.kind === \"invalid\") {\n\t\treturn { failure: unresolvedFailure(providerId, level, \"auth-config-invalid\", \"auth.json is invalid\") };\n\t}\n\n\tconst kimi = providerId === KIMI_PROVIDER_ID ? resolveKimiConfig(options.kimiConfigPath) : {};\n\tif (kimi.failure) {\n\t\treturn {\n\t\t\tfailure: unresolvedFailure(\n\t\t\t\tproviderId,\n\t\t\t\tlevel,\n\t\t\t\t\"kimi-config-invalid\",\n\t\t\t\t`Kimi config TOML is invalid (line ${kimi.failure.line}: ${kimi.failure.reason}); fix the Kimi config or set KIMI_BASE_URL/KIMI_MODEL_NAME`,\n\t\t\t),\n\t\t};\n\t}\n\n\tconst configured = modelsConfig.kind === \"ok\" ? modelsConfig.value.providers[providerId] : undefined;\n\tconst nativeProviders = getProviders();\n\tconst isNative = nativeProviders.includes(providerId as KnownProvider);\n\tconst nativeModels = isNative ? (getModels(providerId as KnownProvider) as Model<Api>[]) : [];\n\tconst configuredModels = configured?.models ?? [];\n\t// At Level 2 the probe model is the paid target and therefore drives endpoint resolution.\n\tconst requestedModel = level === 2 ? (options.probeModelId ?? options.modelId) : options.modelId;\n\tconst configuredModel = requestedModel\n\t\t? configuredModels.find((model) => model.id === requestedModel)\n\t\t: configuredModels[0];\n\n\t// §10.2: with no higher-precedence endpoint (models.json > KIMI_BASE_URL > root TOML), a\n\t// `[providers.<name>]` entry with `type = \"openai_legacy\"` selects a custom OpenAI-compatible\n\t// endpoint, so native-only checks and native credential/model inheritance are skipped downstream.\n\tconst kimiCustomSelected =\n\t\t!(configuredModel?.baseUrl ?? configured?.baseUrl) && !kimi.baseUrl && kimi.customProvider !== undefined;\n\tconst kimiModelName = kimi.modelName ?? (kimiCustomSelected ? kimi.customProvider?.modelName : undefined);\n\tconst endpointModelId = requestedModel ?? kimiModelName;\n\tconst modelIds = kimiCustomSelected\n\t\t? kimiModelName\n\t\t\t? [kimiModelName]\n\t\t\t: []\n\t\t: Array.from(\n\t\t\t\tnew Set([\n\t\t\t\t\t...nativeModels.map((model) => model.id),\n\t\t\t\t\t...configuredModels.map((model) => model.id),\n\t\t\t\t\t...(kimiModelName ? [kimiModelName] : []),\n\t\t\t\t]),\n\t\t\t);\n\tlet nativeModel = kimiCustomSelected\n\t\t? undefined\n\t\t: requestedModel\n\t\t\t? nativeModels.find((model) => model.id === requestedModel)\n\t\t\t: nativeModels[0];\n\tif (!nativeModel && !kimiCustomSelected && requestedModel !== undefined && requestedModel === kimiModelName) {\n\t\t// A native Kimi TOML/env model rides on the provider defaults for endpoint metadata.\n\t\tnativeModel = nativeModels[0];\n\t}\n\n\tlet baseUrl = configuredModel?.baseUrl ?? configured?.baseUrl;\n\tlet api = configuredModel?.api ?? configured?.api ?? nativeModel?.api;\n\tlet source = configured ? \"models.json\" : isNative ? \"built-in-model-registry\" : \"unresolved\";\n\tif (!baseUrl && kimi.baseUrl) {\n\t\tbaseUrl = kimi.baseUrl;\n\t\tsource = kimi.baseUrlSource ?? source;\n\t} else if (kimiCustomSelected && kimi.customProvider) {\n\t\tbaseUrl = kimi.customProvider.baseUrl;\n\t\tapi = \"openai-completions\";\n\t\tsource = `kimi-config-toml:providers.${kimi.customProvider.name}`;\n\t} else {\n\t\tbaseUrl = baseUrl ?? nativeModel?.baseUrl;\n\t}\n\n\tconst storedAuth = authConfig.kind === \"ok\" && authConfig.value[providerId] !== undefined;\n\tconst environmentAuth = findEnvKeys(providerId) !== undefined;\n\tconst configuredAuth = configured?.apiKey ? isConfigValueConfigured(configured.apiKey) : false;\n\tconst headerAuth = configured?.headers !== undefined && Object.keys(configured.headers).length > 0;\n\tconst customApiKey = kimiCustomSelected ? kimi.customProvider?.apiKey : undefined;\n\tconst customAuthPresent = customApiKey !== undefined && isConfigValueConfigured(customApiKey);\n\tconst authPresent = kimiCustomSelected\n\t\t? customAuthPresent\n\t\t: storedAuth || environmentAuth || configuredAuth || headerAuth;\n\tconst authSource = kimiCustomSelected\n\t\t? customAuthPresent\n\t\t\t? source\n\t\t\t: undefined\n\t\t: storedAuth\n\t\t\t? \"auth.json\"\n\t\t\t: environmentAuth\n\t\t\t\t? \"environment\"\n\t\t\t\t: configuredAuth || headerAuth\n\t\t\t\t\t? \"models.json\"\n\t\t\t\t\t: undefined;\n\n\tif (!configured && !isNative && !storedAuth && !environmentAuth) {\n\t\treturn { failure: unresolvedFailure(providerId, level, \"provider-not-found\", \"Provider was not found\") };\n\t}\n\n\tconst origin = classifyOrigin(baseUrl, api, isNative && !kimiCustomSelected);\n\tconst canonicalBaseUrl = canonicalProviderEndpointUrl(baseUrl);\n\tconst binding =\n\t\tcanonicalBaseUrl && api && endpointModelId\n\t\t\t? { origin, source, baseUrl: canonicalBaseUrl, api, modelId: endpointModelId }\n\t\t\t: undefined;\n\tconst createHeaders: ProviderDoctorHeaderFactory | undefined =\n\t\tkimiCustomSelected && binding\n\t\t\t? () => {\n\t\t\t\t\tconst headers = new Headers();\n\t\t\t\t\tif (customApiKey) {\n\t\t\t\t\t\theaders.set(\n\t\t\t\t\t\t\t\"Authorization\",\n\t\t\t\t\t\t\t`Bearer ${resolveConfigValueOrThrow(customApiKey, \"API key for custom Kimi endpoint\")}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn headers;\n\t\t\t\t}\n\t\t\t: undefined;\n\n\treturn {\n\t\ttarget: {\n\t\t\tproviderId,\n\t\t\torigin,\n\t\t\tsource,\n\t\t\tendpoint: { baseUrl, api, modelIds, modelId: endpointModelId },\n\t\t\tauth: { present: authPresent, source: authSource, createHeaders, binding },\n\t\t},\n\t};\n}\n\nexport async function diagnoseResolvedProvider(\n\ttarget: ResolvedProviderTarget,\n\toptions: ProviderDoctorOptions = {},\n\tdependencies: ProviderDoctorDependencies = {},\n): Promise<ProviderDoctorResult> {\n\tconst requestedLevel = options.level ?? 0;\n\tif (requestedLevel !== 0 && requestedLevel !== 1 && requestedLevel !== 2) {\n\t\treturn unresolvedFailure(target.providerId, 0, \"unsupported-level\", \"Provider doctor level is unsupported\");\n\t}\n\tconst level: ProviderDoctorLevel = requestedLevel;\n\t// Level 2 is opt-in only: probeModelId is ignored at levels 0/1, which stay GET-only.\n\tconst probeModelId = level === 2 ? options.probeModelId : undefined;\n\tconst effectiveModelId = probeModelId ?? options.modelId;\n\tconst resolvedTarget = effectiveModelId\n\t\t? { ...target, endpoint: { ...target.endpoint, modelId: effectiveModelId } }\n\t\t: target;\n\tif (level === 2 && options.modelId && probeModelId && options.modelId !== probeModelId) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"model\",\n\t\t\tcode: \"probe-model-conflict\",\n\t\t\tmessage: \"Validation model and paid probe model must match\",\n\t\t};\n\t\treturn publicResult(\n\t\t\tresolvedTarget,\n\t\t\tlevel,\n\t\t\t[{ name: \"model-probe\", status: \"fail\", ...failure }],\n\t\t\tundefined,\n\t\t\tfailure,\n\t\t);\n\t}\n\tif (level === 2 && !probeModelId) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"probe-model-required\",\n\t\t\tmessage: \"Level 2 requires an explicit probe model (--probe-model)\",\n\t\t};\n\t\treturn publicResult(\n\t\t\tresolvedTarget,\n\t\t\tlevel,\n\t\t\t[{ name: \"model-probe\", status: \"fail\", ...failure }],\n\t\t\tundefined,\n\t\t\tfailure,\n\t\t);\n\t}\n\tconst validation = staticChecks(resolvedTarget);\n\tif (validation.failure || level === 0) {\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, validation.failure);\n\t}\n\n\tconst transport = dependencies.transport ?? options.transport;\n\tif (!transport) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"transport-required\",\n\t\t\tmessage: \"Level 1 requires an injected provider transport\",\n\t\t};\n\t\tvalidation.checks.push({ name: \"transport\", status: \"fail\", ...failure });\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, failure);\n\t}\n\tif (resolvedTarget.origin !== \"local-proxy\" && transport.pinsResolvedAddress !== true) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"address-pinning-required\",\n\t\t\tmessage: \"Public provider transport must pin resolved addresses\",\n\t\t};\n\t\tvalidation.checks.push({ name: \"transport\", status: \"fail\", ...failure });\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, failure);\n\t}\n\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tif (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"unexpected-response\",\n\t\t\tmessage: \"Provider probe timeout is invalid\",\n\t\t};\n\t\tvalidation.checks.push({ name: \"transport\", status: \"fail\", ...failure });\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, failure);\n\t}\n\n\tconst addressPolicy: ProviderDoctorAddressPolicy =\n\t\tresolvedTarget.origin === \"local-proxy\"\n\t\t\t? { kind: \"loopback-only\", requireAddressPinning: false }\n\t\t\t: { kind: \"public\", requireAddressPinning: true };\n\tconst resolveAuthHeaders = dependencies.resolveAuthHeaders ?? options.resolveAuthHeaders;\n\tconst credentialBinding = resolvedTarget.auth.binding;\n\tconst boundHeaderFactory = resolvedTarget.auth.createHeaders;\n\tconst endpointBoundHeaders: ProviderDoctorHeaderFactory | undefined =\n\t\tcredentialBinding && boundHeaderFactory\n\t\t\t? async () => {\n\t\t\t\t\tif (!credentialBindingMatchesTarget(credentialBinding, resolvedTarget)) {\n\t\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn await boundHeaderFactory();\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: undefined;\n\tconst createHeaders: ProviderDoctorHeaderFactory = endpointBoundHeaders\n\t\t? endpointBoundHeaders\n\t\t: level === 2 && resolveAuthHeaders\n\t\t\t? async () => {\n\t\t\t\t\tif (credentialBinding && !credentialBindingMatchesTarget(credentialBinding, resolvedTarget)) {\n\t\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn await resolveAuthHeaders(resolvedTarget);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: (resolvedTarget.auth.createHeaders ?? (() => new Headers()));\n\tconst controller = createComposedController(options.signal, timeoutMs);\n\ttry {\n\t\tconst root = await runProbe(\n\t\t\t\"root-endpoint\",\n\t\t\t\"root\",\n\t\t\tmakeRootProbeUrl(validation.url!, resolvedTarget.origin),\n\t\t\ttransport,\n\t\t\taddressPolicy,\n\t\t\tcreateHeaders,\n\t\t\tcontroller,\n\t\t);\n\t\tvalidation.checks.push(root.check);\n\t\tif (root.failure) {\n\t\t\tvalidation.checks.push({\n\t\t\t\tname: \"models-endpoint\",\n\t\t\t\tstatus: \"skipped\",\n\t\t\t\tcode: \"ok\",\n\t\t\t\tmessage: \"Capability probe skipped after root failure\",\n\t\t\t});\n\t\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, root.failure);\n\t\t}\n\n\t\tconst models = await runProbe(\n\t\t\t\"models-endpoint\",\n\t\t\t\"models\",\n\t\t\tappendPath(validation.url!, \"models\"),\n\t\t\ttransport,\n\t\t\taddressPolicy,\n\t\t\tcreateHeaders,\n\t\t\tcontroller,\n\t\t);\n\t\tvalidation.checks.push(models.check);\n\t\tif (level !== 2 || models.failure) {\n\t\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, models.failure);\n\t\t}\n\n\t\tconst spec = modelProbeSpec(resolvedTarget.endpoint.api ?? \"\", probeModelId as string);\n\t\tif (!spec) {\n\t\t\tconst failure: ProbeFailure = {\n\t\t\t\tcategory: \"config\",\n\t\t\t\tcode: \"api-unsupported\",\n\t\t\t\tmessage: \"Model probe supports only OpenAI-compatible APIs\",\n\t\t\t};\n\t\t\tvalidation.checks.push({ name: \"model-probe\", status: \"fail\", ...failure });\n\t\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, failure);\n\t\t}\n\t\tconst probe = await runProbe(\n\t\t\t\"model-probe\",\n\t\t\t\"model-probe\",\n\t\t\tappendPath(validation.url!, spec.path),\n\t\t\ttransport,\n\t\t\taddressPolicy,\n\t\t\tcreateHeaders,\n\t\t\tcontroller,\n\t\t\tspec.payload,\n\t\t);\n\t\tconst dispatched = probe.failure?.code !== \"auth-materialization-failed\";\n\t\tif (dispatched) {\n\t\t\tprobe.check.message = `${probe.check.message}; minimal-token generative probe (may incur provider costs)`;\n\t\t}\n\t\tvalidation.checks.push(probe.check);\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, probe.failure, dispatched);\n\t} finally {\n\t\tcontroller.dispose();\n\t}\n}\n\nexport async function diagnoseProvider(\n\tproviderId: string,\n\toptions: ProviderDoctorOptions = {},\n): Promise<ProviderDoctorResult> {\n\tconst requestedLevel = options.level ?? 0;\n\tif (requestedLevel !== 0 && requestedLevel !== 1 && requestedLevel !== 2) {\n\t\treturn unresolvedFailure(providerId, 0, \"unsupported-level\", \"Provider doctor level is unsupported\");\n\t}\n\tconst resolved = resolveProviderTarget(providerId, options);\n\tif (resolved.failure) return resolved.failure;\n\treturn diagnoseResolvedProvider(resolved.target!, options);\n}\n"]}
1
+ {"version":3,"file":"doctor-provider.d.ts","sourceRoot":"","sources":["../../src/commands/doctor-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { BlockList, isIP } from \"node:net\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { type Api, findEnvKeys, getModels, getProviders, type KnownProvider, type Model } from \"omk-ai\";\nimport { getAgentDir as getConfiguredAgentDir } from \"../config.ts\";\nimport { isConfigValueConfigured, resolveConfigValueOrThrow } from \"../core/resolve-config-value.ts\";\nimport { stripJsonComments } from \"../utils/json.ts\";\nimport { isKnownApi } from \"./provider-known-apis.ts\";\n\nexport type ProviderOrigin = \"native\" | \"custom-openai-compatible\" | \"local-proxy\" | \"unknown\";\nexport type ProviderDoctorLevel = 0 | 1 | 2;\nexport type ProviderDoctorCheckStatus = \"ok\" | \"fail\" | \"skipped\" | \"unsupported\";\nexport type DoctorErrorCategory = \"network\" | \"auth\" | \"model\" | \"config\" | \"server\" | \"unknown\";\n\n/** Exact endpoint-probe category model from the v0.90.9 design (§10.4). */\nexport type EndpointProbeCategory = \"ok\" | \"network\" | \"auth\" | \"unsupported-endpoint\" | \"server\";\n\nexport type EndpointProbeResult = {\n\treachable: boolean;\n\tauthenticated?: boolean;\n\tmodelsSupported?: boolean;\n\tstatus?: number;\n\tcategory: EndpointProbeCategory;\n};\n\nexport type ProviderDoctorCode =\n\t| \"ok\"\n\t| \"provider-not-found\"\n\t| \"models-config-invalid\"\n\t| \"auth-config-invalid\"\n\t| \"kimi-config-invalid\"\n\t| \"unsupported-level\"\n\t| \"model-provider-mismatch\"\n\t| \"api-missing\"\n\t| \"api-unsupported\"\n\t| \"base-url-missing\"\n\t| \"base-url-invalid\"\n\t| \"url-scheme-unsupported\"\n\t| \"origin-unsupported\"\n\t| \"address-policy-blocked\"\n\t| \"auth-missing\"\n\t| \"auth-materialization-failed\"\n\t| \"transport-required\"\n\t| \"address-pinning-required\"\n\t| \"redirect-blocked\"\n\t| \"authentication-failed\"\n\t| \"endpoint-unsupported\"\n\t| \"request-aborted\"\n\t| \"request-timeout\"\n\t| \"network-failure\"\n\t| \"server-error\"\n\t| \"probe-model-required\"\n\t| \"probe-model-conflict\"\n\t| \"unexpected-response\";\n\nexport interface ProviderDoctorEndpoint {\n\tbaseUrl?: string;\n\tapi?: string;\n\tmodelIds?: readonly string[];\n\tmodelId?: string;\n}\n\nexport type ProviderDoctorHeaderFactory = () => Headers | Promise<Headers>;\n\nexport type ProviderDoctorAuthHeaderResolver = (target: ResolvedProviderTarget) => Headers | Promise<Headers>;\n\n/** Fixed-message marker used to classify auth resolution without exposing its cause. */\nexport class ProviderDoctorAuthMaterializationError extends Error {\n\tconstructor() {\n\t\tsuper(\"provider doctor authentication could not be materialized\");\n\t\tthis.name = \"ProviderDoctorAuthMaterializationError\";\n\t}\n}\n\nexport interface ProviderDoctorCredentialBinding {\n\torigin: ProviderOrigin;\n\tsource: string;\n\tbaseUrl: string;\n\tapi: string;\n\tmodelId: string;\n}\n\nexport interface ProviderDoctorAuth {\n\tpresent: boolean;\n\tsource?: string;\n\t/** Materialized by an injected transport at request time only. */\n\tcreateHeaders?: ProviderDoctorHeaderFactory;\n\t/** Exact endpoint provenance that authorizes createHeaders. */\n\tbinding?: ProviderDoctorCredentialBinding;\n}\n\nexport interface ResolvedProviderTarget {\n\tproviderId: string;\n\torigin: ProviderOrigin;\n\tsource: string;\n\tendpoint: ProviderDoctorEndpoint;\n\tauth: ProviderDoctorAuth;\n}\n\n/** Compatibility name for callers using the design-document terminology. */\nexport type ResolvedProviderConfig = ResolvedProviderTarget;\n\nexport type ProviderDoctorAddressPolicy =\n\t| { kind: \"public\"; requireAddressPinning: true }\n\t| { kind: \"loopback-only\"; requireAddressPinning: false };\n\nexport interface ProviderDoctorTransportRequest {\n\turl: URL;\n\tmethod: \"GET\" | \"POST\";\n\tredirect: \"manual\";\n\tsignal: AbortSignal;\n\taddressPolicy: ProviderDoctorAddressPolicy;\n\tcreateHeaders: ProviderDoctorHeaderFactory;\n\t/** Bounded JSON payload; present only for opt-in Level-2 POST model probes. */\n\tbody?: string;\n\tcontentType?: \"application/json\";\n}\n\nexport interface ProviderDoctorTransportResponse {\n\tstatus: number;\n}\n\nexport interface ProviderDoctorTransport {\n\t/** Public requests are rejected unless the transport enforces DNS/address pinning. */\n\tpinsResolvedAddress?: boolean;\n\trequest(request: ProviderDoctorTransportRequest): Promise<ProviderDoctorTransportResponse>;\n}\n\nexport interface ProviderDoctorDependencies {\n\ttransport?: ProviderDoctorTransport;\n\t/** Level-2 only. Called by the transport immediately before each request is dispatched. */\n\tresolveAuthHeaders?: ProviderDoctorAuthHeaderResolver;\n}\n\nexport interface ProviderDoctorOptions extends ProviderDoctorDependencies {\n\tlevel?: ProviderDoctorLevel;\n\tmodelId?: string;\n\t/** Level-2 opt-in only: the model probed with one minimal-token generative request. */\n\tprobeModelId?: string;\n\ttimeoutMs?: number;\n\tsignal?: AbortSignal;\n\tagentDir?: string;\n\t/** Path to the Kimi CLI config TOML consulted for the kimi-coding provider. Defaults to ~/.kimi/config.toml. */\n\tkimiConfigPath?: string;\n}\n\nexport interface ProviderDoctorCheck {\n\tname: string;\n\tstatus: ProviderDoctorCheckStatus;\n\tcode: ProviderDoctorCode;\n\tcategory?: DoctorErrorCategory;\n\tmessage?: string;\n\t/** Exact-category endpoint classification for network probe checks. */\n\tprobe?: EndpointProbeResult;\n}\n\nexport interface ProviderDoctorResult {\n\tprovider: string;\n\tstatus: \"ok\" | \"fail\";\n\tlevel: ProviderDoctorLevel;\n\torigin: ProviderOrigin;\n\tsource: string;\n\t/** Sanitized URL: userinfo, query, and fragment are always removed. */\n\ttargetUrl?: string;\n\t/** Backwards-compatible alias of targetUrl. */\n\tbaseUrl?: string;\n\tapi?: string;\n\tmodelId?: string;\n\tauthPresent: boolean;\n\t/** True only when an opt-in Level-2 model probe was dispatched; the request may incur provider costs. */\n\tcostWarning?: boolean;\n\tchecks: ProviderDoctorCheck[];\n\terror?: {\n\t\tcategory: DoctorErrorCategory;\n\t\tcode: ProviderDoctorCode;\n\t\tmessage: string;\n\t};\n}\n\ninterface ModelEndpointConfig {\n\tid: string;\n\tbaseUrl?: string;\n\tapi?: string;\n}\n\ninterface ProviderConfig {\n\tbaseUrl?: string;\n\tapiKey?: string;\n\tapi?: string;\n\theaders?: Record<string, string>;\n\tmodels?: ModelEndpointConfig[];\n}\n\ninterface ModelsConfig {\n\tproviders: Record<string, ProviderConfig>;\n}\n\ninterface AuthConfigEntry {\n\ttype: \"api_key\" | \"oauth\";\n}\n\ntype AuthConfig = Record<string, AuthConfigEntry>;\n\ntype LoadedConfig<T> = { kind: \"missing\" } | { kind: \"invalid\" } | { kind: \"ok\"; value: T };\n\ntype ProbeFailure = {\n\tcategory: DoctorErrorCategory;\n\tcode: ProviderDoctorCode;\n\tmessage: string;\n};\n\ninterface ProbeOutcome {\n\tcheck: ProviderDoctorCheck;\n\tfailure?: ProbeFailure;\n}\n\nconst DEFAULT_TIMEOUT_MS = 5_000;\nconst OPENAI_COMPATIBLE_APIS = new Set<string>([\"openai-completions\", \"openai-responses\"]);\nconst ABORTED = Symbol(\"provider-doctor-aborted\");\n\n// Family-specific lists: node BlockList canonicalizes IPv4 checks to the v4-mapped v6 form,\n// so a combined list containing ::ffff:0:0/96 would block every IPv4 address.\nconst blockedPublicV4 = new BlockList();\nfor (const [address, prefix] of [\n\t[\"0.0.0.0\", 8],\n\t[\"10.0.0.0\", 8],\n\t[\"100.64.0.0\", 10],\n\t[\"127.0.0.0\", 8],\n\t[\"169.254.0.0\", 16],\n\t[\"172.16.0.0\", 12],\n\t[\"192.168.0.0\", 16],\n\t[\"224.0.0.0\", 4],\n\t[\"240.0.0.0\", 4],\n] as const) {\n\tblockedPublicV4.addSubnet(address, prefix, \"ipv4\");\n}\n// ::ffff:0:0/96 stays here on purpose: a v4-mapped IPv6 literal or DNS answer is anomalous\n// for a public origin and is rejected wholesale (fail closed), covering hex-form mappings too.\nconst blockedPublicV6 = new BlockList();\nfor (const [address, prefix] of [\n\t[\"::\", 128],\n\t[\"::1\", 128],\n\t[\"::ffff:0:0\", 96],\n\t[\"fc00::\", 7],\n\t[\"fe80::\", 10],\n\t[\"ff00::\", 8],\n] as const) {\n\tblockedPublicV6.addSubnet(address, prefix, \"ipv6\");\n}\n\nconst loopbackAddresses = new BlockList();\nloopbackAddresses.addSubnet(\"127.0.0.0\", 8, \"ipv4\");\nloopbackAddresses.addAddress(\"::1\", \"ipv6\");\nloopbackAddresses.addSubnet(\"::ffff:127.0.0.0\", 104, \"ipv6\");\n\n/** True when the literal IP address is a loopback address. Non-IP input is never loopback. */\nexport function isLoopbackAddress(address: string): boolean {\n\tconst family = isIP(address);\n\tif (family === 4) return loopbackAddresses.check(address, \"ipv4\");\n\tif (family === 6) return loopbackAddresses.check(address, \"ipv6\");\n\treturn false;\n}\n\n/** True when the literal IP address must not be dialed for a public origin. Non-IP input fails closed. */\nexport function isBlockedPublicAddress(address: string): boolean {\n\tconst family = isIP(address);\n\tif (family === 4) return blockedPublicV4.check(address, \"ipv4\");\n\tif (family === 6) return blockedPublicV6.check(address, \"ipv6\");\n\treturn true;\n}\n\nexport function getAgentDir(): string {\n\treturn getConfiguredAgentDir();\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isOptionalNonemptyString(value: unknown): value is string | undefined {\n\treturn value === undefined || (typeof value === \"string\" && value.length > 0);\n}\n\nfunction parseModelsConfig(value: unknown): ModelsConfig | undefined {\n\tif (!isRecord(value) || !isRecord(value.providers)) return undefined;\n\tconst providers: Record<string, ProviderConfig> = {};\n\n\tfor (const [providerId, rawProvider] of Object.entries(value.providers)) {\n\t\tif (!isRecord(rawProvider)) return undefined;\n\t\tif (\n\t\t\t!isOptionalNonemptyString(rawProvider.baseUrl) ||\n\t\t\t!isOptionalNonemptyString(rawProvider.apiKey) ||\n\t\t\t!isOptionalNonemptyString(rawProvider.api)\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tlet headers: Record<string, string> | undefined;\n\t\tif (rawProvider.headers !== undefined) {\n\t\t\tif (!isRecord(rawProvider.headers)) return undefined;\n\t\t\theaders = {};\n\t\t\tfor (const [name, headerValue] of Object.entries(rawProvider.headers)) {\n\t\t\t\tif (typeof headerValue !== \"string\") return undefined;\n\t\t\t\theaders[name] = headerValue;\n\t\t\t}\n\t\t}\n\n\t\tlet models: ModelEndpointConfig[] | undefined;\n\t\tif (rawProvider.models !== undefined) {\n\t\t\tif (!Array.isArray(rawProvider.models)) return undefined;\n\t\t\tmodels = [];\n\t\t\tfor (const rawModel of rawProvider.models) {\n\t\t\t\tif (\n\t\t\t\t\t!isRecord(rawModel) ||\n\t\t\t\t\ttypeof rawModel.id !== \"string\" ||\n\t\t\t\t\trawModel.id.length === 0 ||\n\t\t\t\t\t!isOptionalNonemptyString(rawModel.baseUrl) ||\n\t\t\t\t\t!isOptionalNonemptyString(rawModel.api)\n\t\t\t\t) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tmodels.push({ id: rawModel.id, baseUrl: rawModel.baseUrl, api: rawModel.api });\n\t\t\t}\n\t\t}\n\n\t\tproviders[providerId] = {\n\t\t\tbaseUrl: rawProvider.baseUrl,\n\t\t\tapiKey: rawProvider.apiKey,\n\t\t\tapi: rawProvider.api,\n\t\t\theaders,\n\t\t\tmodels,\n\t\t};\n\t}\n\n\treturn { providers };\n}\n\nfunction parseAuthConfig(value: unknown): AuthConfig | undefined {\n\tif (!isRecord(value)) return undefined;\n\tconst auth: AuthConfig = {};\n\tfor (const [providerId, rawEntry] of Object.entries(value)) {\n\t\tif (!isRecord(rawEntry) || (rawEntry.type !== \"api_key\" && rawEntry.type !== \"oauth\")) return undefined;\n\t\tif (rawEntry.type === \"api_key\" && (typeof rawEntry.key !== \"string\" || rawEntry.key.length === 0)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tauth[providerId] = { type: rawEntry.type };\n\t}\n\treturn auth;\n}\n\nconst KIMI_PROVIDER_ID = \"kimi-coding\";\nconst TOML_BASIC_ESCAPES: Record<string, string> = {\n\t'\"': '\"',\n\t\"\\\\\": \"\\\\\",\n\tb: \"\\b\",\n\tf: \"\\f\",\n\tn: \"\\n\",\n\tr: \"\\r\",\n\tt: \"\\t\",\n};\n\ntype TomlQuote = \"basic\" | \"literal\" | \"multiline-basic\" | \"multiline-literal\";\n\ninterface TomlScanState {\n\tquote?: TomlQuote;\n\tcontainers: (\"[\" | \"{\")[];\n}\n\ninterface KimiTomlProviderEntry {\n\tname: string;\n\ttype?: string;\n\tbaseUrl?: string;\n\tmodelName?: string;\n\tapiKey?: string;\n}\n\ninterface KimiConfigValues {\n\tbaseUrl?: string;\n\tmodelName?: string;\n\tproviders: KimiTomlProviderEntry[];\n}\n\ntype KimiConfigLoad =\n\t| { kind: \"missing\" }\n\t| { kind: \"ok\"; value: KimiConfigValues }\n\t| { kind: \"invalid\"; line: number; reason: string };\n\nfunction findBasicStringEnd(text: string, start: number, multiline: boolean): number {\n\tfor (let index = start; index < text.length; index++) {\n\t\tif (text[index] === \"\\\\\") {\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (multiline ? text.startsWith('\"\"\"', index) : text[index] === '\"') return index;\n\t}\n\treturn -1;\n}\n\nfunction decodeTomlBasicString(body: string, multiline: boolean): { value: string } | { error: string } {\n\tlet value = \"\";\n\tfor (let index = 0; index < body.length; index++) {\n\t\tconst char = body[index];\n\t\tif (char !== \"\\\\\") {\n\t\t\tconst code = char.codePointAt(0) ?? 0;\n\t\t\tif (code < 0x20 && char !== \"\\t\" && !(multiline && (char === \"\\n\" || char === \"\\r\"))) {\n\t\t\t\treturn { error: \"invalid control character in string value\" };\n\t\t\t}\n\t\t\tvalue += char;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst escapeChar = body[index + 1];\n\t\tif (escapeChar === undefined) return { error: \"unterminated escape sequence\" };\n\t\tif (multiline && (escapeChar === \"\\n\" || escapeChar === \"\\r\")) {\n\t\t\tindex++;\n\t\t\twhile (index + 1 < body.length && /\\s/.test(body[index + 1])) index++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (escapeChar === \"u\" || escapeChar === \"U\") {\n\t\t\tconst width = escapeChar === \"u\" ? 4 : 8;\n\t\t\tconst hex = body.slice(index + 2, index + 2 + width);\n\t\t\tconst codePoint = /^[0-9a-fA-F]+$/.test(hex) && hex.length === width ? Number.parseInt(hex, 16) : -1;\n\t\t\tif (codePoint < 0 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) {\n\t\t\t\treturn { error: \"invalid unicode escape\" };\n\t\t\t}\n\t\t\tvalue += String.fromCodePoint(codePoint);\n\t\t\tindex += 1 + width;\n\t\t\tcontinue;\n\t\t}\n\t\tconst mapped = TOML_BASIC_ESCAPES[escapeChar];\n\t\tif (mapped === undefined) return { error: \"invalid escape sequence\" };\n\t\tvalue += mapped;\n\t\tindex++;\n\t}\n\treturn { value };\n}\n\n/** Parses one TOML string value. Errors are fixed and never include raw TOML content. */\nfunction parseTomlStringValue(raw: string, requireNonempty = true): { value: string } | { error: string } {\n\tconst text = raw.trim();\n\tconst multilineBasic = text.startsWith('\"\"\"');\n\tconst multilineLiteral = text.startsWith(\"'''\");\n\tconst basic = !multilineBasic && text.startsWith('\"');\n\tconst literal = !multilineLiteral && text.startsWith(\"'\");\n\tif (!multilineBasic && !multilineLiteral && !basic && !literal) {\n\t\treturn { error: \"value must be a quoted string\" };\n\t}\n\n\tconst delimiterWidth = multilineBasic || multilineLiteral ? 3 : 1;\n\tconst end =\n\t\tmultilineBasic || basic\n\t\t\t? findBasicStringEnd(text, delimiterWidth, multilineBasic)\n\t\t\t: text.indexOf(multilineLiteral ? \"'''\" : \"'\", delimiterWidth);\n\tif (end < 0) {\n\t\treturn {\n\t\t\terror:\n\t\t\t\tmultilineBasic || multilineLiteral ? \"unterminated multiline string value\" : \"unterminated string value\",\n\t\t};\n\t}\n\tif (text.slice(end + delimiterWidth).trim().length > 0) {\n\t\treturn { error: \"unexpected content after string value\" };\n\t}\n\n\tlet body = text.slice(delimiterWidth, end);\n\tif ((multilineBasic || multilineLiteral) && body.startsWith(\"\\n\")) body = body.slice(1);\n\tconst parsed =\n\t\tmultilineBasic || basic\n\t\t\t? decodeTomlBasicString(body, multilineBasic)\n\t\t\t: /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/.test(body)\n\t\t\t\t? { error: \"invalid control character in string value\" as const }\n\t\t\t\t: { value: body };\n\tif (\"error\" in parsed) return parsed;\n\treturn requireNonempty && parsed.value.length === 0 ? { error: \"empty string value\" } : parsed;\n}\n\nfunction scanTomlLine(text: string, state: TomlScanState): { content: string; error?: string } {\n\tlet content = \"\";\n\tfor (let index = 0; index < text.length; index++) {\n\t\tconst char = text[index];\n\t\tif (state.quote === \"basic\") {\n\t\t\tcontent += char;\n\t\t\tif (char === \"\\\\\" && index + 1 < text.length) content += text[++index];\n\t\t\telse if (char === '\"') state.quote = undefined;\n\t\t\tcontinue;\n\t\t}\n\t\tif (state.quote === \"literal\") {\n\t\t\tcontent += char;\n\t\t\tif (char === \"'\") state.quote = undefined;\n\t\t\tcontinue;\n\t\t}\n\t\tif (state.quote === \"multiline-basic\") {\n\t\t\tif (text.startsWith('\"\"\"', index)) {\n\t\t\t\tcontent += '\"\"\"';\n\t\t\t\tindex += 2;\n\t\t\t\tstate.quote = undefined;\n\t\t\t} else {\n\t\t\t\tcontent += char;\n\t\t\t\tif (char === \"\\\\\" && index + 1 < text.length) content += text[++index];\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (state.quote === \"multiline-literal\") {\n\t\t\tif (text.startsWith(\"'''\", index)) {\n\t\t\t\tcontent += \"'''\";\n\t\t\t\tindex += 2;\n\t\t\t\tstate.quote = undefined;\n\t\t\t} else {\n\t\t\t\tcontent += char;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"#\") break;\n\t\tif (text.startsWith('\"\"\"', index)) {\n\t\t\tcontent += '\"\"\"';\n\t\t\tindex += 2;\n\t\t\tstate.quote = \"multiline-basic\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (text.startsWith(\"'''\", index)) {\n\t\t\tcontent += \"'''\";\n\t\t\tindex += 2;\n\t\t\tstate.quote = \"multiline-literal\";\n\t\t\tcontinue;\n\t\t}\n\t\tcontent += char;\n\t\tif (char === '\"') state.quote = \"basic\";\n\t\telse if (char === \"'\") state.quote = \"literal\";\n\t\telse if (char === \"[\" || char === \"{\") state.containers.push(char);\n\t\telse if (char === \"]\" || char === \"}\") {\n\t\t\tconst expected = char === \"]\" ? \"[\" : \"{\";\n\t\t\tif (state.containers.pop() !== expected) return { content, error: \"mismatched value delimiter\" };\n\t\t}\n\t}\n\tif (state.quote === \"basic\" || state.quote === \"literal\") {\n\t\treturn { content, error: \"unterminated string value\" };\n\t}\n\treturn { content };\n}\n\nfunction consumeTomlValue(\n\tlines: readonly string[],\n\tstartLine: number,\n\tinitial: string,\n): { value?: string; endLine: number; error?: string } {\n\tconst state: TomlScanState = { containers: [] };\n\tconst chunks: string[] = [];\n\tfor (let line = startLine; line < lines.length; line++) {\n\t\tconst scanned = scanTomlLine(line === startLine ? initial : lines[line], state);\n\t\tchunks.push(scanned.content);\n\t\tif (scanned.error) return { endLine: line, error: scanned.error };\n\t\tif (!state.quote && state.containers.length === 0) {\n\t\t\tconst value = chunks.join(\"\\n\").trim();\n\t\t\treturn value.length > 0\n\t\t\t\t? { value, endLine: line }\n\t\t\t\t: { endLine: startLine, error: \"assignment value is missing\" };\n\t\t}\n\t}\n\treturn {\n\t\tendLine: startLine,\n\t\terror: state.quote ? \"unterminated multiline string value\" : \"unterminated compound value\",\n\t};\n}\n\nfunction findTomlTopLevelCharacter(text: string, wanted: string): number {\n\tconst state: TomlScanState = { containers: [] };\n\tfor (let index = 0; index < text.length; index++) {\n\t\tconst char = text[index];\n\t\tif (state.quote === \"basic\" || state.quote === \"multiline-basic\") {\n\t\t\tif (char === \"\\\\\") index++;\n\t\t\telse if (state.quote === \"basic\" && char === '\"') state.quote = undefined;\n\t\t\telse if (state.quote === \"multiline-basic\" && text.startsWith('\"\"\"', index)) {\n\t\t\t\tstate.quote = undefined;\n\t\t\t\tindex += 2;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (state.quote === \"literal\" || state.quote === \"multiline-literal\") {\n\t\t\tconst delimiter = state.quote === \"literal\" ? \"'\" : \"'''\";\n\t\t\tif (text.startsWith(delimiter, index)) {\n\t\t\t\tstate.quote = undefined;\n\t\t\t\tindex += delimiter.length - 1;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"#\") return -1;\n\t\tif (text.startsWith('\"\"\"', index)) {\n\t\t\tstate.quote = \"multiline-basic\";\n\t\t\tindex += 2;\n\t\t} else if (text.startsWith(\"'''\", index)) {\n\t\t\tstate.quote = \"multiline-literal\";\n\t\t\tindex += 2;\n\t\t} else if (char === '\"') state.quote = \"basic\";\n\t\telse if (char === \"'\") state.quote = \"literal\";\n\t\telse if (char === \"[\" || char === \"{\") state.containers.push(char);\n\t\telse if (char === \"]\" || char === \"}\") state.containers.pop();\n\t\telse if (char === wanted && state.containers.length === 0) return index;\n\t}\n\treturn -1;\n}\n\nfunction parseTomlDottedKey(raw: string): string[] | undefined {\n\tconst components: string[] = [];\n\tlet index = 0;\n\twhile (index < raw.length) {\n\t\twhile (/\\s/.test(raw[index] ?? \"\")) index++;\n\t\tlet component: string | undefined;\n\t\tif (raw[index] === '\"') {\n\t\t\tconst end = findBasicStringEnd(raw, index + 1, false);\n\t\t\tif (end < 0) return undefined;\n\t\t\tconst parsed = parseTomlStringValue(raw.slice(index, end + 1), false);\n\t\t\tif (\"error\" in parsed) return undefined;\n\t\t\tcomponent = parsed.value;\n\t\t\tindex = end + 1;\n\t\t} else if (raw[index] === \"'\") {\n\t\t\tconst end = raw.indexOf(\"'\", index + 1);\n\t\t\tif (end < 0) return undefined;\n\t\t\tcomponent = raw.slice(index + 1, end);\n\t\t\tindex = end + 1;\n\t\t} else {\n\t\t\tconst match = /^[A-Za-z0-9_-]+/.exec(raw.slice(index));\n\t\t\tif (!match) return undefined;\n\t\t\tcomponent = match[0];\n\t\t\tindex += match[0].length;\n\t\t}\n\t\tcomponents.push(component);\n\t\twhile (/\\s/.test(raw[index] ?? \"\")) index++;\n\t\tif (index === raw.length) return components;\n\t\tif (raw[index] !== \".\") return undefined;\n\t\tindex++;\n\t\tif (index === raw.length) return undefined;\n\t}\n\treturn undefined;\n}\n\nfunction splitTomlTopLevel(raw: string): string[] {\n\tconst parts: string[] = [];\n\tlet start = 0;\n\tfor (let index = 0; index < raw.length; index++) {\n\t\tif (raw[index] !== \",\") continue;\n\t\tconst relative = findTomlTopLevelCharacter(raw.slice(start, index + 1), \",\");\n\t\tif (relative !== index - start) continue;\n\t\tparts.push(raw.slice(start, index).trim());\n\t\tstart = index + 1;\n\t}\n\tparts.push(raw.slice(start).trim());\n\treturn parts;\n}\n\nfunction isValidTomlScalar(raw: string): boolean {\n\treturn (\n\t\traw === \"true\" ||\n\t\traw === \"false\" ||\n\t\t/^[+-]?(?:0|[1-9](?:_?\\d)*)$/.test(raw) ||\n\t\t/^[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*$/.test(raw) ||\n\t\t/^[+-]?0o[0-7](?:_?[0-7])*$/.test(raw) ||\n\t\t/^[+-]?0b[01](?:_?[01])*$/.test(raw) ||\n\t\t/^[+-]?(?:(?:0|[1-9](?:_?\\d)*)\\.(?:\\d(?:_?\\d)*)(?:[eE][+-]?\\d(?:_?\\d)*)?|(?:0|[1-9](?:_?\\d)*)[eE][+-]?\\d(?:_?\\d)*|inf|nan)$/.test(\n\t\t\traw,\n\t\t) ||\n\t\t/^\\d{4}-\\d{2}-\\d{2}(?:[Tt ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})?)?$/.test(raw) ||\n\t\t/^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?$/.test(raw)\n\t);\n}\n\nfunction isValidTomlValue(raw: string): boolean {\n\tconst text = raw.trim();\n\tif (text.startsWith('\"') || text.startsWith(\"'\")) return !(\"error\" in parseTomlStringValue(text, false));\n\tif (text.startsWith(\"[\") && text.endsWith(\"]\")) {\n\t\tconst parts = splitTomlTopLevel(text.slice(1, -1));\n\t\tif (parts.at(-1) === \"\") parts.pop();\n\t\treturn parts.every((part) => part.length > 0 && isValidTomlValue(part));\n\t}\n\tif (text.startsWith(\"{\") && text.endsWith(\"}\")) {\n\t\tconst body = text.slice(1, -1).trim();\n\t\tif (body.length === 0) return true;\n\t\treturn splitTomlTopLevel(body).every((part) => {\n\t\t\tconst equals = findTomlTopLevelCharacter(part, \"=\");\n\t\t\treturn equals > 0 && !!parseTomlDottedKey(part.slice(0, equals)) && isValidTomlValue(part.slice(equals + 1));\n\t\t});\n\t}\n\treturn isValidTomlScalar(text);\n}\n\nfunction parseTomlTable(line: string): { components?: string[]; array?: boolean; error?: string } {\n\tconst state: TomlScanState = { containers: [] };\n\tconst scanned = scanTomlLine(line, state);\n\tif (scanned.error || state.quote || state.containers.length > 0) return { error: \"malformed table header\" };\n\tconst text = scanned.content.trim();\n\tconst array = text.startsWith(\"[[\");\n\tif (array ? !text.endsWith(\"]]\") : !text.endsWith(\"]\")) return { error: \"malformed table header\" };\n\tconst inner = text.slice(array ? 2 : 1, array ? -2 : -1).trim();\n\tconst components = parseTomlDottedKey(inner);\n\treturn components && components.length > 0 ? { components, array } : { error: \"malformed table header\" };\n}\n\n/**\n * Strict Kimi TOML subset reader. It validates every table/assignment and generic TOML value,\n * but extracts only root `base_url`/`model_name` and `[providers.<name>]` string fields. Fixed\n * line-number errors ensure credentials and other file content are never echoed.\n */\nfunction loadKimiConfig(path: string): KimiConfigLoad {\n\tif (!existsSync(path)) return { kind: \"missing\" };\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(path, \"utf-8\");\n\t} catch {\n\t\treturn { kind: \"invalid\", line: 0, reason: \"file is unreadable\" };\n\t}\n\tconst value: KimiConfigValues = { providers: [] };\n\tconst lines = content.split(/\\r?\\n/);\n\tconst assignments = new Set<string>();\n\tlet table: KimiTomlProviderEntry | \"root\" | \"other\" = \"root\";\n\tlet scope = \"root\";\n\tfor (let index = 0; index < lines.length; index++) {\n\t\tconst line = lines[index].trim();\n\t\tif (line.length === 0 || line.startsWith(\"#\")) continue;\n\t\tif (line.startsWith(\"[\")) {\n\t\t\tconst parsed = parseTomlTable(line);\n\t\t\tif (parsed.error || !parsed.components) {\n\t\t\t\treturn { kind: \"invalid\", line: index + 1, reason: parsed.error ?? \"malformed table header\" };\n\t\t\t}\n\t\t\tconst [first, name] = parsed.components;\n\t\t\tif (first === \"providers\" && parsed.array) {\n\t\t\t\treturn { kind: \"invalid\", line: index + 1, reason: \"provider table cannot be an array\" };\n\t\t\t}\n\t\t\tif (first === \"providers\" && parsed.components.length === 2 && name !== undefined && name.length > 0) {\n\t\t\t\tconst entry: KimiTomlProviderEntry = { name };\n\t\t\t\tvalue.providers.push(entry);\n\t\t\t\ttable = entry;\n\t\t\t} else {\n\t\t\t\ttable = \"other\";\n\t\t\t}\n\t\t\tscope = `${parsed.array ? \"array\" : \"table\"}:${JSON.stringify(parsed.components)}`;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst assignmentLine = index;\n\t\tconst equals = findTomlTopLevelCharacter(line, \"=\");\n\t\tconst key = equals > 0 ? parseTomlDottedKey(line.slice(0, equals)) : undefined;\n\t\tif (!key || key.length === 0) return { kind: \"invalid\", line: index + 1, reason: \"invalid assignment\" };\n\t\tconst consumed = consumeTomlValue(lines, index, line.slice(equals + 1));\n\t\tif (consumed.error || consumed.value === undefined) {\n\t\t\treturn { kind: \"invalid\", line: assignmentLine + 1, reason: consumed.error ?? \"invalid assignment value\" };\n\t\t}\n\t\tindex = consumed.endLine;\n\t\tif (!isValidTomlValue(consumed.value)) {\n\t\t\treturn { kind: \"invalid\", line: assignmentLine + 1, reason: \"invalid assignment value\" };\n\t\t}\n\t\tconst assignmentId = `${scope}:${JSON.stringify(key)}`;\n\t\tif (assignments.has(assignmentId)) {\n\t\t\treturn { kind: \"invalid\", line: assignmentLine + 1, reason: \"duplicate assignment\" };\n\t\t}\n\t\tassignments.add(assignmentId);\n\t\tif (key.length !== 1 || table === \"other\") continue;\n\n\t\tconst keyName = key[0];\n\t\tconst relevant =\n\t\t\ttable === \"root\"\n\t\t\t\t? keyName === \"base_url\" || keyName === \"model_name\"\n\t\t\t\t: keyName === \"base_url\" ||\n\t\t\t\t\tkeyName === \"model_name\" ||\n\t\t\t\t\tkeyName === \"model\" ||\n\t\t\t\t\tkeyName === \"type\" ||\n\t\t\t\t\tkeyName === \"api_key\";\n\t\tif (!relevant) continue;\n\t\tconst parsedValue = parseTomlStringValue(consumed.value);\n\t\tif (\"error\" in parsedValue) return { kind: \"invalid\", line: assignmentLine + 1, reason: parsedValue.error };\n\t\tif (table === \"root\") {\n\t\t\tif (keyName === \"base_url\") value.baseUrl = parsedValue.value;\n\t\t\telse value.modelName = parsedValue.value;\n\t\t} else {\n\t\t\tif (keyName === \"base_url\") table.baseUrl = parsedValue.value;\n\t\t\telse if (keyName === \"type\") table.type = parsedValue.value;\n\t\t\telse if (keyName === \"api_key\") table.apiKey = parsedValue.value;\n\t\t\telse table.modelName = parsedValue.value;\n\t\t}\n\t}\n\treturn { kind: \"ok\", value };\n}\n\nfunction nonemptyEnv(name: string): string | undefined {\n\tconst value = process.env[name];\n\treturn value && value.length > 0 ? value : undefined;\n}\n\ninterface KimiResolution {\n\tbaseUrl?: string;\n\tmodelName?: string;\n\tbaseUrlSource?: \"kimi-environment\" | \"kimi-config-toml\";\n\t/** First `[providers.<name>]` entry with `type = \"openai_legacy\"` and a base URL (§10.2). */\n\tcustomProvider?: { name: string; baseUrl: string; modelName?: string; apiKey?: string };\n\tfailure?: { line: number; reason: string };\n}\n\nfunction resolveKimiConfig(kimiConfigPath: string | undefined): KimiResolution {\n\tconst path = kimiConfigPath ?? join(homedir(), \".kimi\", \"config.toml\");\n\tconst loaded = loadKimiConfig(path);\n\tif (loaded.kind === \"invalid\") return { failure: { line: loaded.line, reason: loaded.reason } };\n\tconst fileValues: KimiConfigValues = loaded.kind === \"ok\" ? loaded.value : { providers: [] };\n\tconst envBaseUrl = nonemptyEnv(\"KIMI_BASE_URL\");\n\tconst envModelName = nonemptyEnv(\"KIMI_MODEL_NAME\");\n\tconst custom = fileValues.providers.find((entry) => entry.type === \"openai_legacy\" && entry.baseUrl);\n\treturn {\n\t\tbaseUrl: envBaseUrl ?? fileValues.baseUrl,\n\t\tmodelName: envModelName ?? fileValues.modelName,\n\t\tbaseUrlSource: envBaseUrl ? \"kimi-environment\" : fileValues.baseUrl ? \"kimi-config-toml\" : undefined,\n\t\tcustomProvider: custom\n\t\t\t? { name: custom.name, baseUrl: custom.baseUrl as string, modelName: custom.modelName, apiKey: custom.apiKey }\n\t\t\t: undefined,\n\t};\n}\n\nfunction loadConfig<T>(path: string, parse: (value: unknown) => T | undefined, allowComments = false): LoadedConfig<T> {\n\tif (!existsSync(path)) return { kind: \"missing\" };\n\ttry {\n\t\tconst content = readFileSync(path, \"utf-8\");\n\t\tconst parsed = JSON.parse(allowComments ? stripJsonComments(content) : content) as unknown;\n\t\tconst value = parse(parsed);\n\t\treturn value ? { kind: \"ok\", value } : { kind: \"invalid\" };\n\t} catch {\n\t\treturn { kind: \"invalid\" };\n\t}\n}\n\nexport function normalizedUrlHostname(url: URL): string {\n\treturn normalizedHostname(url);\n}\n\nfunction normalizedHostname(url: URL): string {\n\tconst hostname = url.hostname.toLowerCase();\n\tconst unwrapped = hostname.startsWith(\"[\") && hostname.endsWith(\"]\") ? hostname.slice(1, -1) : hostname;\n\treturn unwrapped.endsWith(\".\") ? unwrapped.slice(0, -1) : unwrapped;\n}\n\nfunction isLiteralLoopback(hostname: string): boolean {\n\tif (hostname === \"localhost\" || hostname === \"::1\") return true;\n\tif (isIP(hostname) !== 4) return false;\n\treturn Number(hostname.split(\".\")[0]) === 127;\n}\n\nfunction isBlockedPublicHost(hostname: string): boolean {\n\tif (hostname === \"localhost\" || hostname.endsWith(\".localhost\")) return true;\n\tif (isIP(hostname) === 0) return false;\n\treturn isBlockedPublicAddress(hostname);\n}\n\nfunction sanitizeUrl(rawUrl: string): URL | undefined {\n\ttry {\n\t\tconst url = new URL(rawUrl);\n\t\turl.username = \"\";\n\t\turl.password = \"\";\n\t\turl.search = \"\";\n\t\turl.hash = \"\";\n\t\treturn url;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function canonicalProviderEndpointUrl(rawUrl: string | undefined): string | undefined {\n\tif (!rawUrl) return undefined;\n\tconst url = sanitizeUrl(rawUrl);\n\tif (!url) return undefined;\n\tconst hostname = normalizedHostname(url);\n\tconst host = isIP(hostname) === 6 ? `[${hostname}]` : hostname;\n\tconst pathname = url.pathname.replace(/\\/+$/, \"\") || \"/\";\n\treturn `${url.protocol}//${host}${url.port ? `:${url.port}` : \"\"}${pathname}`;\n}\n\nfunction credentialBindingMatchesTarget(\n\tbinding: ProviderDoctorCredentialBinding,\n\ttarget: ResolvedProviderTarget,\n): boolean {\n\treturn (\n\t\tbinding.origin === target.origin &&\n\t\tbinding.source === target.source &&\n\t\tbinding.baseUrl === canonicalProviderEndpointUrl(target.endpoint.baseUrl) &&\n\t\tbinding.api === target.endpoint.api &&\n\t\tbinding.modelId === target.endpoint.modelId\n\t);\n}\n\nfunction appendPath(url: URL, segment: string): URL {\n\tconst next = URL.parse(url.href);\n\tif (!next) throw new Error(\"validated provider URL could not be cloned\");\n\tnext.pathname = `${next.pathname.replace(/\\/$/, \"\")}/${segment}`;\n\treturn next;\n}\n\nfunction makeRootProbeUrl(url: URL, origin: ProviderOrigin): URL {\n\tconst root = URL.parse(url.href);\n\tif (!root) throw new Error(\"validated provider URL could not be cloned\");\n\tif (origin !== \"local-proxy\") return root;\n\troot.pathname = \"/health\";\n\treturn root;\n}\n\nfunction publicResult(\n\ttarget: ResolvedProviderTarget,\n\tlevel: ProviderDoctorLevel,\n\tchecks: ProviderDoctorCheck[],\n\turl: URL | undefined,\n\tfailure?: ProbeFailure,\n\tcostWarning?: boolean,\n): ProviderDoctorResult {\n\tconst targetUrl = url?.href;\n\treturn {\n\t\tprovider: target.providerId,\n\t\tstatus: failure ? \"fail\" : \"ok\",\n\t\tlevel,\n\t\torigin: target.origin,\n\t\tsource: target.source,\n\t\ttargetUrl,\n\t\tbaseUrl: targetUrl,\n\t\tapi: target.endpoint.api,\n\t\tmodelId: target.endpoint.modelId,\n\t\tauthPresent: target.auth.present,\n\t\t...(costWarning ? { costWarning: true } : {}),\n\t\tchecks,\n\t\terror: failure,\n\t};\n}\n\nfunction unresolvedFailure(\n\tproviderId: string,\n\tlevel: ProviderDoctorLevel,\n\tcode: ProviderDoctorCode,\n\tmessage: string,\n): ProviderDoctorResult {\n\tconst check: ProviderDoctorCheck = { name: \"config\", status: \"fail\", category: \"config\", code, message };\n\treturn {\n\t\tprovider: providerId,\n\t\tstatus: \"fail\",\n\t\tlevel,\n\t\torigin: \"unknown\",\n\t\tsource: \"unresolved\",\n\t\tauthPresent: false,\n\t\tchecks: [check],\n\t\terror: { category: \"config\", code, message },\n\t};\n}\n\nfunction staticChecks(target: ResolvedProviderTarget): {\n\tchecks: ProviderDoctorCheck[];\n\turl?: URL;\n\tfailure?: ProbeFailure;\n} {\n\tconst checks: ProviderDoctorCheck[] = [\n\t\t{ name: \"config-present\", status: \"ok\", code: \"ok\", message: \"Provider configuration resolved\" },\n\t];\n\tif (target.origin === \"custom-openai-compatible\") {\n\t\tchecks.push({\n\t\t\tname: \"native-provider-checks\",\n\t\t\tstatus: \"skipped\",\n\t\t\tcode: \"ok\",\n\t\t\tmessage: \"Custom OpenAI-compatible endpoint; native-only provider checks skipped\",\n\t\t});\n\t}\n\tlet failure: ProbeFailure | undefined;\n\tconst recordFailure = (next: ProbeFailure): void => {\n\t\tfailure ??= next;\n\t};\n\n\tconst requestedModel = target.endpoint.modelId;\n\tconst modelIds = target.endpoint.modelIds ?? [];\n\tif (!requestedModel) {\n\t\tchecks.push({ name: \"model-provider-relation\", status: \"skipped\", code: \"ok\", message: \"No model selected\" });\n\t} else if (!modelIds.includes(requestedModel)) {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"model\",\n\t\t\tcode: \"model-provider-mismatch\",\n\t\t\tmessage: \"Selected model does not belong to the provider\",\n\t\t};\n\t\tchecks.push({ name: \"model-provider-relation\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else {\n\t\tchecks.push({ name: \"model-provider-relation\", status: \"ok\", code: \"ok\", message: \"Model belongs to provider\" });\n\t}\n\n\tconst api = target.endpoint.api;\n\tif (!api) {\n\t\tconst next: ProbeFailure = { category: \"config\", code: \"api-missing\", message: \"Provider API type is missing\" };\n\t\tchecks.push({ name: \"api\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else if (!isKnownApi(api) || (target.origin === \"custom-openai-compatible\" && !OPENAI_COMPATIBLE_APIS.has(api))) {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"api-unsupported\",\n\t\t\tmessage: \"Provider API type is unsupported\",\n\t\t};\n\t\tchecks.push({ name: \"api\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else {\n\t\tchecks.push({ name: \"api\", status: \"ok\", code: \"ok\", message: \"Provider API type is supported\" });\n\t}\n\n\tlet url: URL | undefined;\n\tif (!target.endpoint.baseUrl) {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"base-url-missing\",\n\t\t\tmessage: \"Provider base URL is missing\",\n\t\t};\n\t\tchecks.push({ name: \"base-url\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else {\n\t\turl = sanitizeUrl(target.endpoint.baseUrl);\n\t\tif (!url) {\n\t\t\tconst next: ProbeFailure = {\n\t\t\t\tcategory: \"config\",\n\t\t\t\tcode: \"base-url-invalid\",\n\t\t\t\tmessage: \"Provider base URL is invalid\",\n\t\t\t};\n\t\t\tchecks.push({ name: \"base-url\", status: \"fail\", ...next });\n\t\t\trecordFailure(next);\n\t\t} else {\n\t\t\tconst allowedScheme =\n\t\t\t\ttarget.origin === \"local-proxy\"\n\t\t\t\t\t? url.protocol === \"http:\" || url.protocol === \"https:\"\n\t\t\t\t\t: url.protocol === \"https:\";\n\t\t\tif (!allowedScheme) {\n\t\t\t\tconst next: ProbeFailure = {\n\t\t\t\t\tcategory: \"config\",\n\t\t\t\t\tcode: \"url-scheme-unsupported\",\n\t\t\t\t\tmessage: \"Provider URL scheme is not allowed\",\n\t\t\t\t};\n\t\t\t\tchecks.push({ name: \"base-url\", status: \"fail\", ...next });\n\t\t\t\trecordFailure(next);\n\t\t\t} else {\n\t\t\t\tchecks.push({ name: \"base-url\", status: \"ok\", code: \"ok\", message: \"Provider base URL is valid\" });\n\t\t\t}\n\t\t}\n\t}\n\n\tif (target.origin === \"unknown\") {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"origin-unsupported\",\n\t\t\tmessage: \"Provider origin is unsupported\",\n\t\t};\n\t\tchecks.push({ name: \"origin-policy\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t} else if (url) {\n\t\tconst hostname = normalizedHostname(url);\n\t\tconst blocked = target.origin === \"local-proxy\" ? !isLiteralLoopback(hostname) : isBlockedPublicHost(hostname);\n\t\tif (blocked) {\n\t\t\tconst next: ProbeFailure = {\n\t\t\t\tcategory: \"config\",\n\t\t\t\tcode: \"address-policy-blocked\",\n\t\t\t\tmessage: \"Provider address is blocked by origin policy\",\n\t\t\t};\n\t\t\tchecks.push({ name: \"origin-policy\", status: \"fail\", ...next });\n\t\t\trecordFailure(next);\n\t\t} else {\n\t\t\tchecks.push({ name: \"origin-policy\", status: \"ok\", code: \"ok\", message: \"Provider origin policy passed\" });\n\t\t}\n\t} else {\n\t\tchecks.push({ name: \"origin-policy\", status: \"skipped\", code: \"ok\", message: \"No valid URL to inspect\" });\n\t}\n\n\tif (target.auth.present) {\n\t\tchecks.push({ name: \"auth-present\", status: \"ok\", code: \"ok\", message: \"Authentication is configured\" });\n\t} else if (target.origin === \"custom-openai-compatible\" || target.origin === \"local-proxy\") {\n\t\t// Native login checks do not apply: custom/local OpenAI-compatible endpoints may be keyless.\n\t\tchecks.push({\n\t\t\tname: \"auth-present\",\n\t\t\tstatus: \"skipped\",\n\t\t\tcode: \"ok\",\n\t\t\tmessage: \"No credential configured; custom endpoints may be keyless\",\n\t\t});\n\t} else {\n\t\tconst next: ProbeFailure = {\n\t\t\tcategory: \"auth\",\n\t\t\tcode: \"auth-missing\",\n\t\t\tmessage: \"Authentication is not configured\",\n\t\t};\n\t\tchecks.push({ name: \"auth-present\", status: \"fail\", ...next });\n\t\trecordFailure(next);\n\t}\n\n\treturn { checks, url, failure };\n}\n\ntype ProbeEndpointKind = \"root\" | \"models\" | \"model-probe\";\n\nfunction classifyProbe(name: string, kind: ProbeEndpointKind, status: number): ProbeOutcome {\n\tconst forModels = kind === \"models\";\n\tif (Number.isInteger(status) && status >= 200 && status < 300) {\n\t\tconst probe: EndpointProbeResult = { reachable: true, authenticated: true, status, category: \"ok\" };\n\t\tif (forModels) probe.modelsSupported = true;\n\t\treturn { check: { name, status: \"ok\", code: \"ok\", message: `HTTP ${status}`, probe } };\n\t}\n\tif (status >= 300 && status < 400) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"redirect-blocked\",\n\t\t\tmessage: \"Provider probe returned a blocked redirect\",\n\t\t};\n\t\treturn { check: { name, status: \"fail\", ...failure }, failure };\n\t}\n\tif (status === 401 || status === 403) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"auth\",\n\t\t\tcode: \"authentication-failed\",\n\t\t\tmessage: \"Provider rejected authentication\",\n\t\t};\n\t\tconst probe: EndpointProbeResult = { reachable: true, authenticated: false, status, category: \"auth\" };\n\t\treturn { check: { name, status: \"fail\", ...failure, probe }, failure };\n\t}\n\tif (status === 404 || status === 405 || status === 501) {\n\t\t// A root or /models 404 is neutral: OpenAI-compatible servers may not expose these paths.\n\t\tconst probe: EndpointProbeResult = { reachable: true, status, category: \"unsupported-endpoint\" };\n\t\tif (forModels) probe.modelsSupported = false;\n\t\treturn {\n\t\t\tcheck: {\n\t\t\t\tname,\n\t\t\t\tstatus: \"unsupported\",\n\t\t\t\tcode: \"endpoint-unsupported\",\n\t\t\t\tmessage: `HTTP ${status}; endpoint is not required`,\n\t\t\t\tprobe,\n\t\t\t},\n\t\t};\n\t}\n\tif (status >= 500 && status <= 599) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"server\",\n\t\t\tcode: \"server-error\",\n\t\t\tmessage: \"Provider endpoint returned a server error\",\n\t\t};\n\t\tconst probe: EndpointProbeResult = { reachable: true, status, category: \"server\" };\n\t\treturn { check: { name, status: \"fail\", ...failure, probe }, failure };\n\t}\n\tif (status === 408 || status === 429) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"network\",\n\t\t\tcode: \"network-failure\",\n\t\t\tmessage: \"Provider probe failed at the network boundary\",\n\t\t};\n\t\tconst probe: EndpointProbeResult = { reachable: true, status, category: \"network\" };\n\t\treturn { check: { name, status: \"fail\", ...failure, probe }, failure };\n\t}\n\n\tconst failure: ProbeFailure = {\n\t\tcategory: \"config\",\n\t\tcode: \"unexpected-response\",\n\t\tmessage: \"Provider probe returned an unexpected response\",\n\t};\n\treturn { check: { name, status: \"fail\", ...failure }, failure };\n}\n\nfunction createComposedController(\n\tsignal: AbortSignal | undefined,\n\ttimeoutMs: number,\n): {\n\tcontroller: AbortController;\n\tabortKind: () => \"aborted\" | \"timeout\" | undefined;\n\tdispose: () => void;\n} {\n\tconst controller = new AbortController();\n\tlet kind: \"aborted\" | \"timeout\" | undefined;\n\tconst abortFromCaller = (): void => {\n\t\tkind ??= \"aborted\";\n\t\tcontroller.abort();\n\t};\n\tif (signal?.aborted) abortFromCaller();\n\telse signal?.addEventListener(\"abort\", abortFromCaller, { once: true });\n\tconst timeout = setTimeout(() => {\n\t\tkind ??= \"timeout\";\n\t\tcontroller.abort();\n\t}, timeoutMs);\n\n\treturn {\n\t\tcontroller,\n\t\tabortKind: () => kind,\n\t\tdispose: () => {\n\t\t\tclearTimeout(timeout);\n\t\t\tsignal?.removeEventListener(\"abort\", abortFromCaller);\n\t\t},\n\t};\n}\n\nfunction requestWithAbort(\n\ttransport: ProviderDoctorTransport,\n\trequest: ProviderDoctorTransportRequest,\n): Promise<ProviderDoctorTransportResponse> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (request.signal.aborted) {\n\t\t\treject(ABORTED);\n\t\t\treturn;\n\t\t}\n\t\tconst onAbort = (): void => reject(ABORTED);\n\t\trequest.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\tlet pending: Promise<ProviderDoctorTransportResponse>;\n\t\ttry {\n\t\t\tpending = transport.request(request);\n\t\t} catch (error) {\n\t\t\trequest.signal.removeEventListener(\"abort\", onAbort);\n\t\t\treject(error instanceof ProviderDoctorAuthMaterializationError ? error : undefined);\n\t\t\treturn;\n\t\t}\n\t\tvoid pending.then(\n\t\t\t(response) => {\n\t\t\t\trequest.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\tresolve(response);\n\t\t\t},\n\t\t\t(error: unknown) => {\n\t\t\t\trequest.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\treject(error instanceof ProviderDoctorAuthMaterializationError ? error : undefined);\n\t\t\t},\n\t\t);\n\t});\n}\n\ninterface ProbePayload {\n\tbody: string;\n\tcontentType: \"application/json\";\n}\n\nasync function runProbe(\n\tname: string,\n\tkind: ProbeEndpointKind,\n\turl: URL,\n\ttransport: ProviderDoctorTransport,\n\taddressPolicy: ProviderDoctorAddressPolicy,\n\tcreateHeaders: ProviderDoctorHeaderFactory,\n\tcontroller: ReturnType<typeof createComposedController>,\n\tpayload?: ProbePayload,\n): Promise<ProbeOutcome> {\n\ttry {\n\t\tconst response = await requestWithAbort(transport, {\n\t\t\turl,\n\t\t\tmethod: payload ? \"POST\" : \"GET\",\n\t\t\tredirect: \"manual\",\n\t\t\tsignal: controller.controller.signal,\n\t\t\taddressPolicy,\n\t\t\tcreateHeaders,\n\t\t\tbody: payload?.body,\n\t\t\tcontentType: payload?.contentType,\n\t\t});\n\t\treturn classifyProbe(name, kind, response.status);\n\t} catch (error) {\n\t\tif (error instanceof ProviderDoctorAuthMaterializationError) {\n\t\t\tconst failure: ProbeFailure = {\n\t\t\t\tcategory: \"auth\",\n\t\t\t\tcode: \"auth-materialization-failed\",\n\t\t\t\tmessage: \"Configured authentication could not be materialized; verify auth and config references\",\n\t\t\t};\n\t\t\treturn {\n\t\t\t\tcheck: {\n\t\t\t\t\tname,\n\t\t\t\t\tstatus: \"fail\",\n\t\t\t\t\t...failure,\n\t\t\t\t\tprobe: { reachable: false, authenticated: false, category: \"auth\" },\n\t\t\t\t},\n\t\t\t\tfailure,\n\t\t\t};\n\t\t}\n\t\tconst abortKind = controller.abortKind();\n\t\tconst failure: ProbeFailure = abortKind\n\t\t\t? {\n\t\t\t\t\tcategory: \"network\",\n\t\t\t\t\tcode: abortKind === \"timeout\" ? \"request-timeout\" : \"request-aborted\",\n\t\t\t\t\tmessage: abortKind === \"timeout\" ? \"Provider probe timed out\" : \"Provider probe was aborted\",\n\t\t\t\t}\n\t\t\t: { category: \"network\", code: \"network-failure\", message: \"Provider transport failed\" };\n\t\treturn {\n\t\t\tcheck: { name, status: \"fail\", ...failure, probe: { reachable: false, category: \"network\" } },\n\t\t\tfailure,\n\t\t};\n\t}\n}\n\n/** Minimal-token generative probe payload; never includes tools (no tool-call probe). */\nfunction modelProbeSpec(api: string, modelId: string): { path: string; payload: ProbePayload } | undefined {\n\tif (api === \"openai-completions\") {\n\t\treturn {\n\t\t\tpath: \"chat/completions\",\n\t\t\tpayload: {\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tmodel: modelId,\n\t\t\t\t\tmessages: [{ role: \"user\", content: \"ping\" }],\n\t\t\t\t\tmax_tokens: 1,\n\t\t\t\t\tstream: false,\n\t\t\t\t}),\n\t\t\t\tcontentType: \"application/json\",\n\t\t\t},\n\t\t};\n\t}\n\tif (api === \"openai-responses\") {\n\t\treturn {\n\t\t\tpath: \"responses\",\n\t\t\tpayload: {\n\t\t\t\t// The responses API rejects max_output_tokens below 16; still the minimal request.\n\t\t\t\tbody: JSON.stringify({ model: modelId, input: \"ping\", max_output_tokens: 16, stream: false }),\n\t\t\t\tcontentType: \"application/json\",\n\t\t\t},\n\t\t};\n\t}\n\treturn undefined;\n}\n\nfunction classifyOrigin(baseUrl: string | undefined, api: string | undefined, isNative: boolean): ProviderOrigin {\n\tconst url = baseUrl ? sanitizeUrl(baseUrl) : undefined;\n\tif (url && isLiteralLoopback(normalizedHostname(url))) {\n\t\treturn \"local-proxy\";\n\t}\n\tif (isNative) return \"native\";\n\tif (api && OPENAI_COMPATIBLE_APIS.has(api)) return \"custom-openai-compatible\";\n\treturn \"unknown\";\n}\n\nfunction resolveProviderTarget(\n\tproviderId: string,\n\toptions: ProviderDoctorOptions,\n): { target?: ResolvedProviderTarget; failure?: ProviderDoctorResult } {\n\tconst level = options.level ?? 0;\n\tconst agentDir = options.agentDir ?? getConfiguredAgentDir();\n\tconst modelsConfig = loadConfig(join(agentDir, \"models.json\"), parseModelsConfig, true);\n\tif (modelsConfig.kind === \"invalid\") {\n\t\treturn {\n\t\t\tfailure: unresolvedFailure(providerId, level, \"models-config-invalid\", \"models.json is invalid\"),\n\t\t};\n\t}\n\tconst authConfig = loadConfig(join(agentDir, \"auth.json\"), parseAuthConfig);\n\tif (authConfig.kind === \"invalid\") {\n\t\treturn { failure: unresolvedFailure(providerId, level, \"auth-config-invalid\", \"auth.json is invalid\") };\n\t}\n\n\tconst kimi = providerId === KIMI_PROVIDER_ID ? resolveKimiConfig(options.kimiConfigPath) : {};\n\tif (kimi.failure) {\n\t\treturn {\n\t\t\tfailure: unresolvedFailure(\n\t\t\t\tproviderId,\n\t\t\t\tlevel,\n\t\t\t\t\"kimi-config-invalid\",\n\t\t\t\t`Kimi config TOML is invalid (line ${kimi.failure.line}: ${kimi.failure.reason}); fix the Kimi config or set KIMI_BASE_URL/KIMI_MODEL_NAME`,\n\t\t\t),\n\t\t};\n\t}\n\n\tconst configured = modelsConfig.kind === \"ok\" ? modelsConfig.value.providers[providerId] : undefined;\n\tconst nativeProviders = getProviders();\n\tconst isNative = nativeProviders.includes(providerId as KnownProvider);\n\tconst nativeModels = isNative ? (getModels(providerId as KnownProvider) as Model<Api>[]) : [];\n\tconst configuredModels = configured?.models ?? [];\n\t// At Level 2 the probe model is the paid target and therefore drives endpoint resolution.\n\tconst requestedModel = level === 2 ? (options.probeModelId ?? options.modelId) : options.modelId;\n\tconst configuredModel = requestedModel\n\t\t? configuredModels.find((model) => model.id === requestedModel)\n\t\t: configuredModels[0];\n\n\t// §10.2: with no higher-precedence endpoint (models.json > KIMI_BASE_URL > root TOML), a\n\t// `[providers.<name>]` entry with `type = \"openai_legacy\"` selects a custom OpenAI-compatible\n\t// endpoint, so native-only checks and native credential/model inheritance are skipped downstream.\n\tconst kimiCustomSelected =\n\t\t!(configuredModel?.baseUrl ?? configured?.baseUrl) && !kimi.baseUrl && kimi.customProvider !== undefined;\n\tconst kimiModelName = kimi.modelName ?? (kimiCustomSelected ? kimi.customProvider?.modelName : undefined);\n\tconst endpointModelId = requestedModel ?? kimiModelName;\n\tconst modelIds = kimiCustomSelected\n\t\t? kimiModelName\n\t\t\t? [kimiModelName]\n\t\t\t: []\n\t\t: Array.from(\n\t\t\t\tnew Set([\n\t\t\t\t\t...nativeModels.map((model) => model.id),\n\t\t\t\t\t...configuredModels.map((model) => model.id),\n\t\t\t\t\t...(kimiModelName ? [kimiModelName] : []),\n\t\t\t\t]),\n\t\t\t);\n\tlet nativeModel = kimiCustomSelected\n\t\t? undefined\n\t\t: requestedModel\n\t\t\t? nativeModels.find((model) => model.id === requestedModel)\n\t\t\t: nativeModels[0];\n\tif (!nativeModel && !kimiCustomSelected && requestedModel !== undefined && requestedModel === kimiModelName) {\n\t\t// A native Kimi TOML/env model rides on the provider defaults for endpoint metadata.\n\t\tnativeModel = nativeModels[0];\n\t}\n\n\tlet baseUrl = configuredModel?.baseUrl ?? configured?.baseUrl;\n\tlet api = configuredModel?.api ?? configured?.api ?? nativeModel?.api;\n\tlet source = configured ? \"models.json\" : isNative ? \"built-in-model-registry\" : \"unresolved\";\n\tif (!baseUrl && kimi.baseUrl) {\n\t\tbaseUrl = kimi.baseUrl;\n\t\tsource = kimi.baseUrlSource ?? source;\n\t} else if (kimiCustomSelected && kimi.customProvider) {\n\t\tbaseUrl = kimi.customProvider.baseUrl;\n\t\tapi = \"openai-completions\";\n\t\tsource = `kimi-config-toml:providers.${kimi.customProvider.name}`;\n\t} else {\n\t\tbaseUrl = baseUrl ?? nativeModel?.baseUrl;\n\t}\n\n\tconst storedAuth = authConfig.kind === \"ok\" && authConfig.value[providerId] !== undefined;\n\tconst environmentAuth = findEnvKeys(providerId) !== undefined;\n\tconst configuredAuth = configured?.apiKey ? isConfigValueConfigured(configured.apiKey) : false;\n\tconst headerAuth = configured?.headers !== undefined && Object.keys(configured.headers).length > 0;\n\tconst customApiKey = kimiCustomSelected ? kimi.customProvider?.apiKey : undefined;\n\tconst customAuthPresent = customApiKey !== undefined && isConfigValueConfigured(customApiKey);\n\tconst authPresent = kimiCustomSelected\n\t\t? customAuthPresent\n\t\t: storedAuth || environmentAuth || configuredAuth || headerAuth;\n\tconst authSource = kimiCustomSelected\n\t\t? customAuthPresent\n\t\t\t? source\n\t\t\t: undefined\n\t\t: storedAuth\n\t\t\t? \"auth.json\"\n\t\t\t: environmentAuth\n\t\t\t\t? \"environment\"\n\t\t\t\t: configuredAuth || headerAuth\n\t\t\t\t\t? \"models.json\"\n\t\t\t\t\t: undefined;\n\n\tif (!configured && !isNative && !storedAuth && !environmentAuth) {\n\t\treturn { failure: unresolvedFailure(providerId, level, \"provider-not-found\", \"Provider was not found\") };\n\t}\n\n\tconst origin = classifyOrigin(baseUrl, api, isNative && !kimiCustomSelected);\n\tconst canonicalBaseUrl = canonicalProviderEndpointUrl(baseUrl);\n\tconst binding =\n\t\tcanonicalBaseUrl && api && endpointModelId\n\t\t\t? { origin, source, baseUrl: canonicalBaseUrl, api, modelId: endpointModelId }\n\t\t\t: undefined;\n\tconst createHeaders: ProviderDoctorHeaderFactory | undefined =\n\t\tkimiCustomSelected && binding\n\t\t\t? () => {\n\t\t\t\t\tconst headers = new Headers();\n\t\t\t\t\tif (customApiKey) {\n\t\t\t\t\t\theaders.set(\n\t\t\t\t\t\t\t\"Authorization\",\n\t\t\t\t\t\t\t`Bearer ${resolveConfigValueOrThrow(customApiKey, \"API key for custom Kimi endpoint\")}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn headers;\n\t\t\t\t}\n\t\t\t: undefined;\n\n\treturn {\n\t\ttarget: {\n\t\t\tproviderId,\n\t\t\torigin,\n\t\t\tsource,\n\t\t\tendpoint: { baseUrl, api, modelIds, modelId: endpointModelId },\n\t\t\tauth: { present: authPresent, source: authSource, createHeaders, binding },\n\t\t},\n\t};\n}\n\nexport async function diagnoseResolvedProvider(\n\ttarget: ResolvedProviderTarget,\n\toptions: ProviderDoctorOptions = {},\n\tdependencies: ProviderDoctorDependencies = {},\n): Promise<ProviderDoctorResult> {\n\tconst requestedLevel = options.level ?? 0;\n\tif (requestedLevel !== 0 && requestedLevel !== 1 && requestedLevel !== 2) {\n\t\treturn unresolvedFailure(target.providerId, 0, \"unsupported-level\", \"Provider doctor level is unsupported\");\n\t}\n\tconst level: ProviderDoctorLevel = requestedLevel;\n\t// Level 2 is opt-in only: probeModelId is ignored at levels 0/1, which stay GET-only.\n\tconst probeModelId = level === 2 ? options.probeModelId : undefined;\n\tconst effectiveModelId = probeModelId ?? options.modelId;\n\tconst resolvedTarget = effectiveModelId\n\t\t? { ...target, endpoint: { ...target.endpoint, modelId: effectiveModelId } }\n\t\t: target;\n\tif (level === 2 && options.modelId && probeModelId && options.modelId !== probeModelId) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"model\",\n\t\t\tcode: \"probe-model-conflict\",\n\t\t\tmessage: \"Validation model and paid probe model must match\",\n\t\t};\n\t\treturn publicResult(\n\t\t\tresolvedTarget,\n\t\t\tlevel,\n\t\t\t[{ name: \"model-probe\", status: \"fail\", ...failure }],\n\t\t\tundefined,\n\t\t\tfailure,\n\t\t);\n\t}\n\tif (level === 2 && !probeModelId) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"probe-model-required\",\n\t\t\tmessage: \"Level 2 requires an explicit probe model (--probe-model)\",\n\t\t};\n\t\treturn publicResult(\n\t\t\tresolvedTarget,\n\t\t\tlevel,\n\t\t\t[{ name: \"model-probe\", status: \"fail\", ...failure }],\n\t\t\tundefined,\n\t\t\tfailure,\n\t\t);\n\t}\n\tconst validation = staticChecks(resolvedTarget);\n\tif (validation.failure || level === 0) {\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, validation.failure);\n\t}\n\n\tconst transport = dependencies.transport ?? options.transport;\n\tif (!transport) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"transport-required\",\n\t\t\tmessage: \"Level 1 requires an injected provider transport\",\n\t\t};\n\t\tvalidation.checks.push({ name: \"transport\", status: \"fail\", ...failure });\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, failure);\n\t}\n\tif (resolvedTarget.origin !== \"local-proxy\" && transport.pinsResolvedAddress !== true) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"address-pinning-required\",\n\t\t\tmessage: \"Public provider transport must pin resolved addresses\",\n\t\t};\n\t\tvalidation.checks.push({ name: \"transport\", status: \"fail\", ...failure });\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, failure);\n\t}\n\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\tif (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n\t\tconst failure: ProbeFailure = {\n\t\t\tcategory: \"config\",\n\t\t\tcode: \"unexpected-response\",\n\t\t\tmessage: \"Provider probe timeout is invalid\",\n\t\t};\n\t\tvalidation.checks.push({ name: \"transport\", status: \"fail\", ...failure });\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, failure);\n\t}\n\n\tconst addressPolicy: ProviderDoctorAddressPolicy =\n\t\tresolvedTarget.origin === \"local-proxy\"\n\t\t\t? { kind: \"loopback-only\", requireAddressPinning: false }\n\t\t\t: { kind: \"public\", requireAddressPinning: true };\n\tconst resolveAuthHeaders = dependencies.resolveAuthHeaders ?? options.resolveAuthHeaders;\n\tconst credentialBinding = resolvedTarget.auth.binding;\n\tconst boundHeaderFactory = resolvedTarget.auth.createHeaders;\n\tconst endpointBoundHeaders: ProviderDoctorHeaderFactory | undefined =\n\t\tcredentialBinding && boundHeaderFactory\n\t\t\t? async () => {\n\t\t\t\t\tif (!credentialBindingMatchesTarget(credentialBinding, resolvedTarget)) {\n\t\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn await boundHeaderFactory();\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: undefined;\n\tconst createHeaders: ProviderDoctorHeaderFactory = endpointBoundHeaders\n\t\t? endpointBoundHeaders\n\t\t: level === 2 && resolveAuthHeaders\n\t\t\t? async () => {\n\t\t\t\t\tif (credentialBinding && !credentialBindingMatchesTarget(credentialBinding, resolvedTarget)) {\n\t\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn await resolveAuthHeaders(resolvedTarget);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthrow new ProviderDoctorAuthMaterializationError();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t: (resolvedTarget.auth.createHeaders ?? (() => new Headers()));\n\tconst controller = createComposedController(options.signal, timeoutMs);\n\ttry {\n\t\tconst root = await runProbe(\n\t\t\t\"root-endpoint\",\n\t\t\t\"root\",\n\t\t\tmakeRootProbeUrl(validation.url!, resolvedTarget.origin),\n\t\t\ttransport,\n\t\t\taddressPolicy,\n\t\t\tcreateHeaders,\n\t\t\tcontroller,\n\t\t);\n\t\tvalidation.checks.push(root.check);\n\t\tif (root.failure) {\n\t\t\tvalidation.checks.push({\n\t\t\t\tname: \"models-endpoint\",\n\t\t\t\tstatus: \"skipped\",\n\t\t\t\tcode: \"ok\",\n\t\t\t\tmessage: \"Capability probe skipped after root failure\",\n\t\t\t});\n\t\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, root.failure);\n\t\t}\n\n\t\tconst models = await runProbe(\n\t\t\t\"models-endpoint\",\n\t\t\t\"models\",\n\t\t\tappendPath(validation.url!, \"models\"),\n\t\t\ttransport,\n\t\t\taddressPolicy,\n\t\t\tcreateHeaders,\n\t\t\tcontroller,\n\t\t);\n\t\tvalidation.checks.push(models.check);\n\t\tif (level !== 2 || models.failure) {\n\t\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, models.failure);\n\t\t}\n\n\t\tconst spec = modelProbeSpec(resolvedTarget.endpoint.api ?? \"\", probeModelId as string);\n\t\tif (!spec) {\n\t\t\tconst failure: ProbeFailure = {\n\t\t\t\tcategory: \"config\",\n\t\t\t\tcode: \"api-unsupported\",\n\t\t\t\tmessage: \"Model probe supports only OpenAI-compatible APIs\",\n\t\t\t};\n\t\t\tvalidation.checks.push({ name: \"model-probe\", status: \"fail\", ...failure });\n\t\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, failure);\n\t\t}\n\t\tconst probe = await runProbe(\n\t\t\t\"model-probe\",\n\t\t\t\"model-probe\",\n\t\t\tappendPath(validation.url!, spec.path),\n\t\t\ttransport,\n\t\t\taddressPolicy,\n\t\t\tcreateHeaders,\n\t\t\tcontroller,\n\t\t\tspec.payload,\n\t\t);\n\t\tconst dispatched = probe.failure?.code !== \"auth-materialization-failed\";\n\t\tif (dispatched) {\n\t\t\tprobe.check.message = `${probe.check.message}; minimal-token generative probe (may incur provider costs)`;\n\t\t}\n\t\tvalidation.checks.push(probe.check);\n\t\treturn publicResult(resolvedTarget, level, validation.checks, validation.url, probe.failure, dispatched);\n\t} finally {\n\t\tcontroller.dispose();\n\t}\n}\n\nexport async function diagnoseProvider(\n\tproviderId: string,\n\toptions: ProviderDoctorOptions = {},\n): Promise<ProviderDoctorResult> {\n\tconst requestedLevel = options.level ?? 0;\n\tif (requestedLevel !== 0 && requestedLevel !== 1 && requestedLevel !== 2) {\n\t\treturn unresolvedFailure(providerId, 0, \"unsupported-level\", \"Provider doctor level is unsupported\");\n\t}\n\tconst resolved = resolveProviderTarget(providerId, options);\n\tif (resolved.failure) return resolved.failure;\n\treturn diagnoseResolvedProvider(resolved.target!, options);\n}\n"]}
@@ -6,6 +6,7 @@ import { findEnvKeys, getModels, getProviders } from "omk-ai";
6
6
  import { getAgentDir as getConfiguredAgentDir } from "../config.js";
7
7
  import { isConfigValueConfigured, resolveConfigValueOrThrow } from "../core/resolve-config-value.js";
8
8
  import { stripJsonComments } from "../utils/json.js";
9
+ import { isKnownApi } from "./provider-known-apis.js";
9
10
  /** Fixed-message marker used to classify auth resolution without exposing its cause. */
10
11
  export class ProviderDoctorAuthMaterializationError extends Error {
11
12
  constructor() {
@@ -14,17 +15,6 @@ export class ProviderDoctorAuthMaterializationError extends Error {
14
15
  }
15
16
  }
16
17
  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
18
  const OPENAI_COMPATIBLE_APIS = new Set(["openai-completions", "openai-responses"]);
29
19
  const ABORTED = Symbol("provider-doctor-aborted");
30
20
  // Family-specific lists: node BlockList canonicalizes IPv4 checks to the v4-mapped v6 form,
@@ -759,8 +749,7 @@ function staticChecks(target) {
759
749
  checks.push({ name: "api", status: "fail", ...next });
760
750
  recordFailure(next);
761
751
  }
762
- else if (!KNOWN_APIS.has(api) ||
763
- (target.origin === "custom-openai-compatible" && !OPENAI_COMPATIBLE_APIS.has(api))) {
752
+ else if (!isKnownApi(api) || (target.origin === "custom-openai-compatible" && !OPENAI_COMPATIBLE_APIS.has(api))) {
764
753
  const next = {
765
754
  category: "config",
766
755
  code: "api-unsupported",