patina-cli 3.11.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.patina.default.yaml +29 -29
- package/CHANGELOG.md +53 -0
- package/NOTICE +21 -0
- package/README.md +117 -224
- package/README_JA.md +134 -77
- package/README_KR.md +132 -74
- package/README_ZH.md +137 -80
- package/SKILL.md +11 -20
- package/artifacts/rebaseline-2025/README.md +147 -0
- package/artifacts/rebaseline-2025/human-controls.public.jsonl +250 -0
- package/artifacts/rebaseline-2025/intake.example.jsonl +2 -0
- package/artifacts/rebaseline-2025/intake.local.example.jsonl +25 -0
- package/artifacts/rebaseline-2025/prompts.template.jsonl +7 -0
- package/artifacts/rebaseline-2025/sources.ko-public.jsonl +39 -0
- package/assets/brand/patina-badge.svg +18 -0
- package/assets/brand/patina-mark.svg +8 -0
- package/assets/demo/README.md +79 -0
- package/core/scoring.md +12 -12
- package/core/standalone-prompt.md +3 -1
- package/core/stylometry.md +93 -22
- package/docs/API.md +1554 -0
- package/docs/AUTHENTICATION.md +50 -26
- package/docs/AUTHENTICATION_KR.md +54 -29
- package/docs/BRANDING.md +9 -8
- package/docs/CLI.md +55 -14
- package/docs/COOKBOOK.md +8 -21
- package/docs/DEMO.md +32 -5
- package/docs/EXIT-CODES.md +2 -3
- package/docs/FALSE-POSITIVES.md +63 -0
- package/docs/FAQ.md +9 -1
- package/docs/FAQ_KR.md +3 -1
- package/docs/FLAG-PARITY.md +33 -47
- package/docs/ISSUE-WAVES.md +57 -0
- package/docs/PATTERNS-EN.md +67 -3
- package/docs/PATTERNS-JA.md +68 -2
- package/docs/PATTERNS-KO.md +70 -7
- package/docs/PATTERNS-ZH.md +67 -3
- package/docs/PATTERNS.md +5 -5
- package/docs/RESEARCH-DOCS-PLATFORM.md +54 -0
- package/docs/ROADMAP.md +46 -66
- package/docs/TRANSLATIONESE-KO.md +51 -0
- package/docs/audits/2026-05-deep-research.md +3 -1
- package/docs/benchmarks/README.md +51 -0
- package/docs/benchmarks/detector-comparison.json +69 -9
- package/docs/benchmarks/detector-comparison.md +10 -5
- package/docs/benchmarks/katfish-ko-latest.json +657 -0
- package/docs/benchmarks/katfish-ko-latest.md +77 -0
- package/docs/benchmarks/latest.json +1183 -108
- package/docs/benchmarks/latest.md +84 -60
- package/docs/benchmarks/lexicon-freshness-en-2026-05-22.json +1121 -0
- package/docs/benchmarks/lexicon-freshness-en-2026-05-22.md +136 -0
- package/docs/benchmarks/rebaseline-latest.json +381 -0
- package/docs/benchmarks/rebaseline-latest.md +121 -0
- package/docs/benchmarks/register-stratified-latest.json +164 -0
- package/docs/benchmarks/register-stratified-latest.md +99 -0
- package/docs/benchmarks/register-stratified.md +43 -0
- package/docs/integrations/github-action.md +44 -11
- package/docs/integrations/playground.md +58 -0
- package/docs/integrations/pre-commit.md +5 -5
- package/docs/integrations/release.md +5 -3
- package/docs/integrations/static-sites.md +83 -0
- package/docs/research/2025-rebaseline-plan.md +71 -2
- package/docs/research/2026-rebaseline.md +102 -0
- package/docs/research/adversarial-mps.md +41 -0
- package/docs/research/ai-human-metrics.md +35 -23
- package/docs/research/human-eval-panel.md +42 -0
- package/docs/research/judge-agreement.md +24 -0
- package/docs/research/ko-2025-corpus-sources.md +135 -0
- package/docs/research/lexicon-freshness-audit.md +64 -0
- package/docs/research/zh-ja-lexicon-calibration.md +60 -0
- package/docs/social/patina-launch-copy.md +173 -100
- package/docs/social/patina-launch-execution.md +94 -0
- package/docs/social/patina-launch-korean-first.md +83 -0
- package/docs/social/signs-of-ai-writing.md +26 -0
- package/docs/social/signs-of-ai-writing_KR.md +26 -0
- package/lexicon/ai-en.md +21 -24
- package/lexicon/ai-ja.md +158 -0
- package/lexicon/ai-ko.md +9 -9
- package/lexicon/ai-zh.md +158 -0
- package/lexicon/provenance/ai-en.json +970 -0
- package/lexicon/provenance/ai-ja.json +542 -0
- package/lexicon/provenance/ai-ko.json +866 -0
- package/lexicon/provenance/ai-zh.json +542 -0
- package/package.json +49 -8
- package/patterns/en-communication.md +5 -0
- package/patterns/en-content.md +5 -0
- package/patterns/en-filler.md +5 -0
- package/patterns/en-language.md +29 -1
- package/patterns/en-structure.md +5 -0
- package/patterns/en-style.md +5 -0
- package/patterns/en-viral-hook.md +42 -2
- package/patterns/ja-communication.md +5 -0
- package/patterns/ja-content.md +5 -0
- package/patterns/ja-filler.md +5 -0
- package/patterns/ja-language.md +33 -1
- package/patterns/ja-structure.md +12 -0
- package/patterns/ja-style.md +5 -0
- package/patterns/ja-viral-hook.md +41 -2
- package/patterns/ko-communication.md +5 -0
- package/patterns/ko-content.md +5 -0
- package/patterns/ko-filler.md +5 -0
- package/patterns/ko-language.md +33 -1
- package/patterns/ko-structure.md +25 -6
- package/patterns/ko-style.md +5 -0
- package/patterns/ko-viral-hook.md +38 -2
- package/patterns/zh-communication.md +5 -0
- package/patterns/zh-content.md +5 -0
- package/patterns/zh-filler.md +5 -0
- package/patterns/zh-language.md +37 -1
- package/patterns/zh-structure.md +12 -0
- package/patterns/zh-style.md +5 -0
- package/patterns/zh-viral-hook.md +38 -2
- package/playground/README.md +55 -0
- package/playground/analytics.js +4 -0
- package/playground/analyzer.js +883 -0
- package/playground/app.js +157 -0
- package/playground/data/lexicons.js +343 -0
- package/playground/index.html +138 -0
- package/playground/styles.css +267 -0
- package/profiles/namuwiki.md +111 -0
- package/scripts/adversarial-mps-report.mjs +201 -0
- package/scripts/badge-json.mjs +79 -0
- package/scripts/benchmark-report.mjs +56 -9
- package/scripts/check-release-metadata.mjs +0 -2
- package/scripts/detector-comparison.mjs +7 -7
- package/scripts/generate-playground-data.mjs +77 -0
- package/scripts/katfish-calibration.mjs +464 -0
- package/scripts/lexicon-freshness.mjs +485 -0
- package/scripts/lint.mjs +1 -1
- package/scripts/precommit-score.mjs +4 -3
- package/scripts/prose-score.mjs +81 -5
- package/scripts/rebaseline-intake.mjs +242 -0
- package/scripts/rebaseline-score.mjs +268 -0
- package/scripts/rebaseline-summary.mjs +773 -0
- package/scripts/rebaseline-web-collect.mjs +410 -0
- package/scripts/update-benchmark-ranges.mjs +1 -0
- package/src/api.js +69 -105
- package/src/auth.js +50 -2
- package/src/backends/claude-cli.js +19 -4
- package/src/backends/codex-cli.js +19 -3
- package/src/backends/contract.js +230 -1
- package/src/backends/gemini-cli.js +18 -5
- package/src/backends/index.js +87 -12
- package/src/backends/kimi-cli.js +161 -0
- package/src/cli.js +577 -567
- package/src/commands/doctor.js +2 -2
- package/src/config.js +29 -0
- package/src/errors.js +53 -1
- package/src/features/discourse-tells.js +68 -0
- package/src/features/index.js +82 -8
- package/src/features/lexicon.js +40 -6
- package/src/features/markup-leakage.js +69 -0
- package/src/features/segment.js +41 -0
- package/src/features/signal-strength.js +81 -0
- package/src/features/stylometry.js +231 -1
- package/src/features/translationese.js +127 -0
- package/src/loader.js +76 -0
- package/src/logger.js +22 -23
- package/src/model-defaults.js +55 -0
- package/src/ouroboros.js +31 -0
- package/src/output.js +102 -90
- package/src/prompt-builder.js +103 -68
- package/src/providers.js +51 -4
- package/src/scoring.js +210 -2
- package/src/security.js +75 -0
- package/tests/fixtures/live-quality/en/public-docs-01.md +26 -0
- package/tests/fixtures/live-quality/ko/public-docs-01.md +26 -0
- package/tests/fixtures/suspect-zones/expected-ranges.json +207 -16
- package/tests/fixtures/suspect-zones/ja/ai/ja-ai-04-lexicon.md +11 -0
- package/tests/fixtures/suspect-zones/ja/natural/ja-nat-04-lexicon-cold.md +11 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-02.md +4 -5
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-07-ko-diagnostic.md +11 -0
- package/tests/fixtures/suspect-zones/zh/ai/zh-ai-04-lexicon.md +11 -0
- package/tests/fixtures/suspect-zones/zh/natural/zh-nat-04-lexicon-cold.md +11 -0
- package/tests/quality/README.md +188 -11
- package/tests/quality/adversarial-mps/fixtures.jsonl +10 -0
- package/tests/quality/benchmark.mjs +39 -1
- package/tests/quality/dogfood.mjs +5 -3
- package/tests/quality/live-fixtures.jsonl +2 -0
- package/tests/quality/live-quality.mjs +596 -0
- package/tests/quality/ranking-metrics.mjs +136 -0
- package/tests/quality/rebaseline-manifest.example.jsonl +5 -0
- package/vercel.json +53 -0
- package/SKILL-MAX.md +0 -455
- package/docs/internal/HARNESS.md +0 -14
- package/docs/internal/README.md +0 -14
- package/docs/internal/WARP.md +0 -23
- package/patina-max/SKILL.md +0 -523
- package/patina-max/composite.py +0 -457
- package/src/cache.js +0 -106
- package/src/commands/init.js +0 -208
- package/src/manifest.js +0 -162
- package/src/max-mode.js +0 -207
package/src/auth.js
CHANGED
|
@@ -1,26 +1,63 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { inputError } from './errors.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Environment variable names checked for HTTP provider authentication.
|
|
6
|
+
*
|
|
7
|
+
* @type {string[]}
|
|
8
|
+
* @example
|
|
9
|
+
* const supported = HTTP_KEY_ENV_VARS.includes('OPENAI_API_KEY');
|
|
10
|
+
*/
|
|
4
11
|
export const HTTP_KEY_ENV_VARS = [
|
|
5
12
|
'PATINA_API_KEY',
|
|
6
13
|
'OPENAI_API_KEY',
|
|
7
14
|
'GEMINI_API_KEY',
|
|
8
15
|
'GROQ_API_KEY',
|
|
9
16
|
'TOGETHER_API_KEY',
|
|
17
|
+
'KIMI_API_KEY',
|
|
18
|
+
'MOONSHOT_API_KEY',
|
|
10
19
|
];
|
|
11
20
|
|
|
12
21
|
// Default openai-http runs against the OpenAI-compatible default endpoint, so
|
|
13
22
|
// only generic/OpenAI keys make it authenticated without an explicit provider.
|
|
23
|
+
/**
|
|
24
|
+
* Default key lookup order for the OpenAI-compatible HTTP provider.
|
|
25
|
+
*
|
|
26
|
+
* @type {string[]}
|
|
27
|
+
* @example
|
|
28
|
+
* const first = DEFAULT_HTTP_KEY_ENV_VARS[0]; // PATINA_API_KEY
|
|
29
|
+
*/
|
|
14
30
|
export const DEFAULT_HTTP_KEY_ENV_VARS = [
|
|
15
31
|
'PATINA_API_KEY',
|
|
16
32
|
'OPENAI_API_KEY',
|
|
17
33
|
];
|
|
18
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Build the key lookup order for a selected provider.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [providerApiKeyEnv] Provider-specific key env var, such as GEMINI_API_KEY.
|
|
39
|
+
* @returns {string[]} Unique env var names in lookup order.
|
|
40
|
+
* @throws {Error} Does not intentionally throw; invalid non-string env names can still propagate JavaScript runtime failures.
|
|
41
|
+
* @example
|
|
42
|
+
* const vars = providerHttpKeyEnvVars('GEMINI_API_KEY');
|
|
43
|
+
*/
|
|
19
44
|
export function providerHttpKeyEnvVars(providerApiKeyEnv) {
|
|
20
45
|
if (!providerApiKeyEnv) return DEFAULT_HTTP_KEY_ENV_VARS;
|
|
21
46
|
return uniqueEnvVars([providerApiKeyEnv, 'PATINA_API_KEY']);
|
|
22
47
|
}
|
|
23
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Inspect where an HTTP API key would be read from without exposing the secret.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} [options] Inspection options.
|
|
53
|
+
* @param {object} [options.env=process.env] Environment map to inspect.
|
|
54
|
+
* @param {Function} [options.readFile] File reader for PATINA_API_KEY_FILE.
|
|
55
|
+
* @param {string[]} [options.envVars=DEFAULT_HTTP_KEY_ENV_VARS] Env vars to check.
|
|
56
|
+
* @returns {{ok: boolean, source: string|null, envVars: string[], filePath: string|null, detail: string}} Source diagnostics.
|
|
57
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
58
|
+
* @example
|
|
59
|
+
* const source = inspectHttpApiKeySource({ env: { PATINA_API_KEY: 'sk-...' } });
|
|
60
|
+
*/
|
|
24
61
|
export function inspectHttpApiKeySource({
|
|
25
62
|
env = process.env,
|
|
26
63
|
readFile = readFileSync,
|
|
@@ -48,8 +85,20 @@ export function inspectHttpApiKeySource({
|
|
|
48
85
|
};
|
|
49
86
|
}
|
|
50
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the HTTP API key from a key file or environment.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} [options] Resolution options.
|
|
92
|
+
* @param {string} [options.apiKeyFile] Explicit key file path.
|
|
93
|
+
* @param {object} [options.env=process.env] Environment map.
|
|
94
|
+
* @param {Function} [options.readFile] File reader for key files.
|
|
95
|
+
* @param {string[]} [options.envVars=DEFAULT_HTTP_KEY_ENV_VARS] Env var lookup order.
|
|
96
|
+
* @returns {string|undefined} Resolved key value, or undefined when unauthenticated.
|
|
97
|
+
* @throws {PatinaCliError} When the configured key file cannot be read or is empty.
|
|
98
|
+
* @example
|
|
99
|
+
* const key = resolveHttpApiKey({ env: process.env });
|
|
100
|
+
*/
|
|
51
101
|
export function resolveHttpApiKey({
|
|
52
|
-
explicitApiKey,
|
|
53
102
|
apiKeyFile,
|
|
54
103
|
env = process.env,
|
|
55
104
|
readFile = readFileSync,
|
|
@@ -68,7 +117,6 @@ export function resolveHttpApiKey({
|
|
|
68
117
|
return file.key;
|
|
69
118
|
}
|
|
70
119
|
|
|
71
|
-
if (explicitApiKey) return explicitApiKey;
|
|
72
120
|
|
|
73
121
|
const source = inspectHttpApiKeySource({ env, readFile, envVars });
|
|
74
122
|
return source.ok && source.source !== 'PATINA_API_KEY_FILE'
|
|
@@ -2,9 +2,12 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import { homedir, tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { DEFAULT_BACKEND_TIMEOUT_MS } from './contract.js';
|
|
5
|
+
import { DEFAULT_BACKEND_TIMEOUT_MS, runInteractiveCommand } from './contract.js';
|
|
6
|
+
import { resolveLocalCliModel } from '../model-defaults.js';
|
|
6
7
|
|
|
7
8
|
export const name = 'claude-cli';
|
|
9
|
+
export const loginCommand = 'claude auth login';
|
|
10
|
+
export const installHint = 'Install Claude Code first, then run `patina auth login claude-cli` again.';
|
|
8
11
|
|
|
9
12
|
export function isAvailable() {
|
|
10
13
|
try {
|
|
@@ -23,22 +26,34 @@ export function isAuthenticated() {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export function authHint() {
|
|
26
|
-
return
|
|
29
|
+
return `Run \`${loginCommand}\` and follow the OAuth prompt to authenticate (uses your Claude subscription, no API key needed).`;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
export
|
|
32
|
+
export function login(options = {}) {
|
|
33
|
+
return runInteractiveCommand({
|
|
34
|
+
backendName: name,
|
|
35
|
+
command: 'claude',
|
|
36
|
+
args: ['auth', 'login'],
|
|
37
|
+
notFoundHint: installHint,
|
|
38
|
+
...options,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function invoke({ prompt, model, modelSource, signal, timeout = DEFAULT_BACKEND_TIMEOUT_MS } = {}) {
|
|
30
43
|
if (!prompt || typeof prompt !== 'string') {
|
|
31
44
|
throw new Error('claude-cli backend: prompt must be a non-empty string');
|
|
32
45
|
}
|
|
33
46
|
throwIfAborted(signal);
|
|
34
47
|
|
|
48
|
+
const cliModel = resolveLocalCliModel({ backendName: name, model, modelSource });
|
|
49
|
+
|
|
35
50
|
// Spawn from a fresh temp directory so a prompt-injection in user text
|
|
36
51
|
// cannot read or write inside the caller's repo. claude -p prints to
|
|
37
52
|
// stdout, so no output file plumbing is needed (unlike codex-cli).
|
|
38
53
|
const dir = mkdtempSync(join(tmpdir(), 'patina-claude-'));
|
|
39
54
|
|
|
40
55
|
return new Promise((resolve, reject) => {
|
|
41
|
-
const proc = spawn('claude', ['-p'], { stdio: ['pipe', 'pipe', 'pipe'], cwd: dir });
|
|
56
|
+
const proc = spawn('claude', ['-p', '--model', cliModel], { stdio: ['pipe', 'pipe', 'pipe'], cwd: dir });
|
|
42
57
|
|
|
43
58
|
let stdout = '';
|
|
44
59
|
let stderr = '';
|
|
@@ -2,9 +2,12 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { homedir, tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { DEFAULT_BACKEND_TIMEOUT_MS } from './contract.js';
|
|
5
|
+
import { DEFAULT_BACKEND_TIMEOUT_MS, runInteractiveCommand } from './contract.js';
|
|
6
|
+
import { resolveLocalCliModel } from '../model-defaults.js';
|
|
6
7
|
|
|
7
8
|
export const name = 'codex-cli';
|
|
9
|
+
export const loginCommand = 'codex login';
|
|
10
|
+
export const installHint = 'Install it from https://github.com/openai/codex, then run `patina auth login codex-cli` again.';
|
|
8
11
|
|
|
9
12
|
export function isAvailable() {
|
|
10
13
|
try {
|
|
@@ -20,15 +23,27 @@ export function isAuthenticated() {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function authHint() {
|
|
23
|
-
return
|
|
26
|
+
return `Run \`${loginCommand}\` to authenticate (uses your ChatGPT Plus account, no API key needed).`;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export
|
|
29
|
+
export function login(options = {}) {
|
|
30
|
+
return runInteractiveCommand({
|
|
31
|
+
backendName: name,
|
|
32
|
+
command: 'codex',
|
|
33
|
+
args: ['login'],
|
|
34
|
+
notFoundHint: installHint,
|
|
35
|
+
...options,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function invoke({ prompt, model, modelSource, signal, timeout = DEFAULT_BACKEND_TIMEOUT_MS } = {}) {
|
|
27
40
|
if (!prompt || typeof prompt !== 'string') {
|
|
28
41
|
throw new Error('codex-cli backend: prompt must be a non-empty string');
|
|
29
42
|
}
|
|
30
43
|
throwIfAborted(signal);
|
|
31
44
|
|
|
45
|
+
const cliModel = resolveLocalCliModel({ backendName: name, model, modelSource });
|
|
46
|
+
|
|
32
47
|
// Run codex from a fresh temp directory with the read-only sandbox so that
|
|
33
48
|
// a prompt-injection in user text cannot read the caller's repo or write
|
|
34
49
|
// arbitrary files. The output file lives inside the same temp dir so codex
|
|
@@ -42,6 +57,7 @@ export async function invoke({ prompt, signal, timeout = DEFAULT_BACKEND_TIMEOUT
|
|
|
42
57
|
'--skip-git-repo-check',
|
|
43
58
|
'--sandbox', 'read-only',
|
|
44
59
|
'-C', dir,
|
|
60
|
+
'--model', cliModel,
|
|
45
61
|
'--output-last-message', outFile,
|
|
46
62
|
], { stdio: ['pipe', 'pipe', 'pipe'], cwd: dir });
|
|
47
63
|
|
package/src/backends/contract.js
CHANGED
|
@@ -1,4 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_BACKEND_TIMEOUT_MS = 600_000;
|
|
7
|
+
export const DEFAULT_HTTP_MAX_RETRIES = 2;
|
|
8
|
+
export const PROMPT_SIZE_WARNING_CHARS = 20_000;
|
|
9
|
+
|
|
10
|
+
export const BACKEND_SAFETY_DEFAULTS = Object.freeze({
|
|
11
|
+
'openai-http': {
|
|
12
|
+
maxConcurrency: 4,
|
|
13
|
+
maxRetries: DEFAULT_HTTP_MAX_RETRIES,
|
|
14
|
+
promptMode: 'strict',
|
|
15
|
+
agentRuntime: false,
|
|
16
|
+
},
|
|
17
|
+
'codex-cli': {
|
|
18
|
+
maxConcurrency: 2,
|
|
19
|
+
maxRetries: 0,
|
|
20
|
+
promptMode: 'minimal',
|
|
21
|
+
agentRuntime: true,
|
|
22
|
+
},
|
|
23
|
+
'claude-cli': {
|
|
24
|
+
maxConcurrency: 1,
|
|
25
|
+
maxRetries: 0,
|
|
26
|
+
promptMode: 'minimal',
|
|
27
|
+
agentRuntime: true,
|
|
28
|
+
},
|
|
29
|
+
'gemini-cli': {
|
|
30
|
+
maxConcurrency: 2,
|
|
31
|
+
maxRetries: 0,
|
|
32
|
+
promptMode: 'minimal',
|
|
33
|
+
agentRuntime: true,
|
|
34
|
+
},
|
|
35
|
+
'kimi-cli': {
|
|
36
|
+
maxConcurrency: 1,
|
|
37
|
+
maxRetries: 0,
|
|
38
|
+
promptMode: 'minimal',
|
|
39
|
+
agentRuntime: true,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const UNKNOWN_BACKEND_SAFETY = Object.freeze({
|
|
44
|
+
maxConcurrency: Infinity,
|
|
45
|
+
maxRetries: 0,
|
|
46
|
+
promptMode: 'strict',
|
|
47
|
+
agentRuntime: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export function getBackendSafety(backendName) {
|
|
51
|
+
return BACKEND_SAFETY_DEFAULTS[backendName] || UNKNOWN_BACKEND_SAFETY;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveBackendMaxConcurrency(backendName, override) {
|
|
55
|
+
const n = override === undefined || override === null
|
|
56
|
+
? getBackendSafety(backendName).maxConcurrency
|
|
57
|
+
: Number(override);
|
|
58
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : Infinity;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveBackendMaxRetries(backendName, override) {
|
|
62
|
+
const n = override === undefined || override === null
|
|
63
|
+
? getBackendSafety(backendName).maxRetries
|
|
64
|
+
: Number(override);
|
|
65
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
|
|
66
|
+
}
|
|
2
67
|
|
|
3
68
|
export function isRetryableBackendError(err, { attemptIndex = 0, signal } = {}) {
|
|
4
69
|
if (signal?.aborted) return false;
|
|
@@ -10,6 +75,8 @@ export function isRetryableBackendError(err, { attemptIndex = 0, signal } = {})
|
|
|
10
75
|
export function describeBackendError(err) {
|
|
11
76
|
const status = extractStatus(err);
|
|
12
77
|
if (status) return `HTTP ${status}`;
|
|
78
|
+
const exitCode = extractExitCode(err);
|
|
79
|
+
if (exitCode) return `exit code ${exitCode}`;
|
|
13
80
|
return err?.name || 'error';
|
|
14
81
|
}
|
|
15
82
|
|
|
@@ -19,3 +86,165 @@ function extractStatus(err) {
|
|
|
19
86
|
const match = String(err?.message || '').match(/\bHTTP\s+(429|503)\b/);
|
|
20
87
|
return match ? Number(match[1]) : null;
|
|
21
88
|
}
|
|
89
|
+
|
|
90
|
+
function extractExitCode(err) {
|
|
91
|
+
const direct = Number(err?.code);
|
|
92
|
+
if (Number.isFinite(direct)) return direct;
|
|
93
|
+
const match = String(err?.message || '').match(/\bexited with code\s+(\d+)\b/i);
|
|
94
|
+
return match ? Number(match[1]) : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function withBackendConcurrencySlot({
|
|
98
|
+
backendName,
|
|
99
|
+
maxConcurrency,
|
|
100
|
+
signal,
|
|
101
|
+
timeout = DEFAULT_BACKEND_TIMEOUT_MS,
|
|
102
|
+
pollMs = 250,
|
|
103
|
+
staleMs = Math.max(timeout * 2, 30 * 60_000),
|
|
104
|
+
fn,
|
|
105
|
+
} = {}) {
|
|
106
|
+
if (typeof fn !== 'function') {
|
|
107
|
+
throw new Error('backend concurrency slot requires fn');
|
|
108
|
+
}
|
|
109
|
+
if (!Number.isFinite(maxConcurrency)) {
|
|
110
|
+
return fn();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const slot = await acquireBackendSlot({
|
|
114
|
+
backendName,
|
|
115
|
+
maxConcurrency,
|
|
116
|
+
signal,
|
|
117
|
+
timeout,
|
|
118
|
+
pollMs,
|
|
119
|
+
staleMs,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
return await fn();
|
|
124
|
+
} finally {
|
|
125
|
+
releaseBackendSlot(slot);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function acquireBackendSlot({
|
|
130
|
+
backendName,
|
|
131
|
+
maxConcurrency,
|
|
132
|
+
signal,
|
|
133
|
+
timeout,
|
|
134
|
+
pollMs,
|
|
135
|
+
staleMs,
|
|
136
|
+
}) {
|
|
137
|
+
throwIfAborted(signal, `${backendName || 'backend'}: aborted while waiting for concurrency slot`);
|
|
138
|
+
const startedAt = Date.now();
|
|
139
|
+
const root = join(tmpdir(), 'patina-backend-slots', safePathSegment(backendName || 'backend'));
|
|
140
|
+
mkdirSync(root, { recursive: true });
|
|
141
|
+
|
|
142
|
+
for (;;) {
|
|
143
|
+
for (let index = 0; index < maxConcurrency; index++) {
|
|
144
|
+
const slot = join(root, `slot-${index}`);
|
|
145
|
+
cleanupStaleSlot(slot, staleMs);
|
|
146
|
+
try {
|
|
147
|
+
mkdirSync(slot);
|
|
148
|
+
writeFileSync(join(slot, 'owner.json'), JSON.stringify({
|
|
149
|
+
pid: process.pid,
|
|
150
|
+
backendName,
|
|
151
|
+
createdAt: new Date().toISOString(),
|
|
152
|
+
}), 'utf8');
|
|
153
|
+
return slot;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (err?.code !== 'EEXIST') throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throwIfAborted(signal, `${backendName || 'backend'}: aborted while waiting for concurrency slot`);
|
|
160
|
+
if (Date.now() - startedAt >= timeout) {
|
|
161
|
+
throw new Error(`${backendName || 'backend'}: timed out waiting for concurrency slot (cap ${maxConcurrency})`);
|
|
162
|
+
}
|
|
163
|
+
await sleepWithAbort(Math.min(pollMs, timeout), signal, backendName);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function cleanupStaleSlot(slot, staleMs) {
|
|
168
|
+
try {
|
|
169
|
+
const ageMs = Date.now() - statSync(slot).mtimeMs;
|
|
170
|
+
if (ageMs > staleMs) rmSync(slot, { recursive: true, force: true });
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function releaseBackendSlot(slot) {
|
|
175
|
+
try {
|
|
176
|
+
rmSync(slot, { recursive: true, force: true });
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sleepWithAbort(ms, signal, backendName) {
|
|
181
|
+
if (ms <= 0) return Promise.resolve();
|
|
182
|
+
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const timer = setTimeout(() => {
|
|
185
|
+
cleanup();
|
|
186
|
+
resolve();
|
|
187
|
+
}, ms);
|
|
188
|
+
const onAbort = () => {
|
|
189
|
+
cleanup();
|
|
190
|
+
reject(abortError(`${backendName || 'backend'}: aborted while waiting for concurrency slot`));
|
|
191
|
+
};
|
|
192
|
+
const cleanup = () => {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
signal.removeEventListener('abort', onAbort);
|
|
195
|
+
};
|
|
196
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function throwIfAborted(signal, message) {
|
|
201
|
+
if (signal?.aborted) throw abortError(message);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function abortError(message) {
|
|
205
|
+
const err = new Error(message);
|
|
206
|
+
err.name = 'AbortError';
|
|
207
|
+
return err;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function safePathSegment(value) {
|
|
211
|
+
return String(value).replace(/[^a-z0-9._-]+/gi, '_');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function runInteractiveCommand({
|
|
215
|
+
backendName,
|
|
216
|
+
command,
|
|
217
|
+
args = [],
|
|
218
|
+
cwd = process.cwd(),
|
|
219
|
+
env = process.env,
|
|
220
|
+
stdio = 'inherit',
|
|
221
|
+
notFoundHint,
|
|
222
|
+
} = {}) {
|
|
223
|
+
if (!backendName || !command) {
|
|
224
|
+
throw new Error('interactive backend command requires backendName and command');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return new Promise((resolve, reject) => {
|
|
228
|
+
const proc = spawn(command, args, { cwd, env, stdio });
|
|
229
|
+
|
|
230
|
+
proc.on('error', (err) => {
|
|
231
|
+
if (err.code === 'ENOENT') {
|
|
232
|
+
reject(new Error(`${backendName}: \`${command}\` CLI not found. ${notFoundHint || 'Install the CLI and try again.'}`));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
reject(new Error(`${backendName}: failed to spawn ${command} (${err.message})`));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
proc.on('close', (code, signal) => {
|
|
239
|
+
if (code === 0) {
|
|
240
|
+
resolve();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (signal) {
|
|
244
|
+
reject(new Error(`${backendName}: ${command} was terminated by ${signal}`));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
reject(new Error(`${backendName}: ${command} exited with code ${code}`));
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -2,9 +2,12 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import { homedir, tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { DEFAULT_BACKEND_TIMEOUT_MS } from './contract.js';
|
|
5
|
+
import { DEFAULT_BACKEND_TIMEOUT_MS, runInteractiveCommand } from './contract.js';
|
|
6
|
+
import { resolveLocalCliModel } from '../model-defaults.js';
|
|
6
7
|
|
|
7
8
|
export const name = 'gemini-cli';
|
|
9
|
+
export const loginCommand = 'gemini';
|
|
10
|
+
export const installHint = 'Install Gemini CLI first, then run `patina auth login gemini-cli` again.';
|
|
8
11
|
|
|
9
12
|
export function isAvailable() {
|
|
10
13
|
try {
|
|
@@ -28,10 +31,20 @@ export function authHint() {
|
|
|
28
31
|
if (process.env.GEMINI_API_KEY) {
|
|
29
32
|
return 'Authenticated via GEMINI_API_KEY env var.';
|
|
30
33
|
}
|
|
31
|
-
return
|
|
34
|
+
return `Run \`${loginCommand}\` once interactively to log in via Google OAuth, or set GEMINI_API_KEY.`;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
export
|
|
37
|
+
export function login(options = {}) {
|
|
38
|
+
return runInteractiveCommand({
|
|
39
|
+
backendName: name,
|
|
40
|
+
command: 'gemini',
|
|
41
|
+
args: [],
|
|
42
|
+
notFoundHint: installHint,
|
|
43
|
+
...options,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function invoke({ prompt, model, modelSource, signal, timeout = DEFAULT_BACKEND_TIMEOUT_MS } = {}) {
|
|
35
48
|
if (!prompt || typeof prompt !== 'string') {
|
|
36
49
|
throw new Error('gemini-cli backend: prompt must be a non-empty string');
|
|
37
50
|
}
|
|
@@ -43,8 +56,8 @@ export async function invoke({ prompt, model, signal, timeout = DEFAULT_BACKEND_
|
|
|
43
56
|
// --skip-trust is required because the temp dir isn't in gemini's trusted
|
|
44
57
|
// workspace list (otherwise gemini exits 55).
|
|
45
58
|
const dir = mkdtempSync(join(tmpdir(), 'patina-gemini-'));
|
|
46
|
-
const
|
|
47
|
-
|
|
59
|
+
const cliModel = resolveLocalCliModel({ backendName: name, model, modelSource });
|
|
60
|
+
const args = ['-p', '', '--output-format', 'text', '--skip-trust', '-m', cliModel];
|
|
48
61
|
|
|
49
62
|
return new Promise((resolve, reject) => {
|
|
50
63
|
const proc = spawn('gemini', args, { stdio: ['pipe', 'pipe', 'pipe'], cwd: dir });
|
package/src/backends/index.js
CHANGED
|
@@ -2,12 +2,18 @@ import { callLLM } from '../api.js';
|
|
|
2
2
|
import * as codexCli from './codex-cli.js';
|
|
3
3
|
import * as claudeCli from './claude-cli.js';
|
|
4
4
|
import * as geminiCli from './gemini-cli.js';
|
|
5
|
+
import * as kimiCli from './kimi-cli.js';
|
|
5
6
|
import { inspectHttpApiKeySource } from '../auth.js';
|
|
6
7
|
import { inputError } from '../errors.js';
|
|
8
|
+
import { DEFAULT_BEST_MODELS } from '../model-defaults.js';
|
|
7
9
|
import {
|
|
8
10
|
DEFAULT_BACKEND_TIMEOUT_MS,
|
|
9
11
|
describeBackendError,
|
|
12
|
+
getBackendSafety,
|
|
10
13
|
isRetryableBackendError,
|
|
14
|
+
resolveBackendMaxConcurrency,
|
|
15
|
+
resolveBackendMaxRetries,
|
|
16
|
+
withBackendConcurrencySlot,
|
|
11
17
|
} from './contract.js';
|
|
12
18
|
|
|
13
19
|
const openaiHttp = {
|
|
@@ -22,12 +28,12 @@ const openaiHttp = {
|
|
|
22
28
|
model,
|
|
23
29
|
signal,
|
|
24
30
|
timeout = DEFAULT_BACKEND_TIMEOUT_MS,
|
|
31
|
+
maxRetries,
|
|
25
32
|
temperature,
|
|
26
33
|
seed,
|
|
27
34
|
onResponse,
|
|
28
|
-
cache,
|
|
29
35
|
}) =>
|
|
30
|
-
callLLM({ prompt, apiKey, baseURL, model, signal, timeout, temperature, seed, onResponse
|
|
36
|
+
callLLM({ prompt, apiKey, baseURL, model, signal, timeout, maxRetries, temperature, seed, onResponse }),
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
const REGISTRY = {
|
|
@@ -35,16 +41,57 @@ const REGISTRY = {
|
|
|
35
41
|
'codex-cli': codexCli,
|
|
36
42
|
'claude-cli': claudeCli,
|
|
37
43
|
'gemini-cli': geminiCli,
|
|
44
|
+
'kimi-cli': kimiCli,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const BACKEND_META = {
|
|
48
|
+
'openai-http': {
|
|
49
|
+
kind: 'http',
|
|
50
|
+
selectWith: 'default, --backend openai-http, --provider <name>',
|
|
51
|
+
defaultModel: DEFAULT_BEST_MODELS.openai,
|
|
52
|
+
},
|
|
53
|
+
'codex-cli': {
|
|
54
|
+
kind: 'local-cli',
|
|
55
|
+
selectWith: '--backend codex-cli, --model codex-*',
|
|
56
|
+
defaultModel: DEFAULT_BEST_MODELS.codexCli,
|
|
57
|
+
},
|
|
58
|
+
'claude-cli': {
|
|
59
|
+
kind: 'local-cli',
|
|
60
|
+
selectWith: '--backend claude-cli, --model claude-*',
|
|
61
|
+
defaultModel: DEFAULT_BEST_MODELS.claudeCli,
|
|
62
|
+
},
|
|
63
|
+
'gemini-cli': {
|
|
64
|
+
kind: 'local-cli',
|
|
65
|
+
selectWith: '--backend gemini-cli, --model gemini-*',
|
|
66
|
+
defaultModel: DEFAULT_BEST_MODELS.geminiCli,
|
|
67
|
+
},
|
|
68
|
+
'kimi-cli': {
|
|
69
|
+
kind: 'local-cli',
|
|
70
|
+
selectWith: '--backend kimi-cli, --model kimi-*',
|
|
71
|
+
defaultModel: DEFAULT_BEST_MODELS.kimiCli,
|
|
72
|
+
},
|
|
38
73
|
};
|
|
39
74
|
|
|
40
75
|
export function listBackends() {
|
|
41
76
|
return Object.keys(REGISTRY).map((key) => {
|
|
42
77
|
const b = REGISTRY[key];
|
|
78
|
+
const meta = BACKEND_META[key] || { kind: 'unknown', selectWith: `--backend ${key}` };
|
|
79
|
+
const safety = getBackendSafety(key);
|
|
43
80
|
return {
|
|
44
81
|
name: key,
|
|
82
|
+
kind: meta.kind,
|
|
83
|
+
selectWith: meta.selectWith,
|
|
84
|
+
defaultModel: meta.defaultModel || null,
|
|
85
|
+
safety,
|
|
86
|
+
maxConcurrency: safety.maxConcurrency,
|
|
87
|
+
maxRetries: safety.maxRetries,
|
|
88
|
+
promptMode: safety.promptMode,
|
|
89
|
+
agentRuntime: safety.agentRuntime,
|
|
45
90
|
available: b.isAvailable(),
|
|
46
91
|
authenticated: b.isAuthenticated(),
|
|
47
92
|
authHint: b.authHint(),
|
|
93
|
+
loginCommand: b.loginCommand || null,
|
|
94
|
+
installHint: b.installHint || null,
|
|
48
95
|
};
|
|
49
96
|
});
|
|
50
97
|
}
|
|
@@ -53,21 +100,26 @@ export function listBackendNames() {
|
|
|
53
100
|
return Object.keys(REGISTRY);
|
|
54
101
|
}
|
|
55
102
|
|
|
56
|
-
export function selectBackend({ name, model } = {}) {
|
|
103
|
+
export function selectBackend({ name, model, modelSource } = {}) {
|
|
57
104
|
if (name) {
|
|
58
105
|
const backend = resolveBackend(name);
|
|
59
106
|
return { backend, autoSelected: false, reason: 'explicit' };
|
|
60
107
|
}
|
|
61
108
|
|
|
62
|
-
|
|
109
|
+
const useModelHeuristic = model && (modelSource === undefined || modelSource === 'flag');
|
|
110
|
+
|
|
111
|
+
if (useModelHeuristic && /^codex(-|$)/i.test(model)) {
|
|
63
112
|
return { backend: REGISTRY['codex-cli'], autoSelected: false, reason: 'model heuristic' };
|
|
64
113
|
}
|
|
65
|
-
if (
|
|
114
|
+
if (useModelHeuristic && /^claude(-|$)/i.test(model)) {
|
|
66
115
|
return { backend: REGISTRY['claude-cli'], autoSelected: false, reason: 'model heuristic' };
|
|
67
116
|
}
|
|
68
|
-
if (
|
|
117
|
+
if (useModelHeuristic && /^gemini(-|$)/i.test(model)) {
|
|
69
118
|
return { backend: REGISTRY['gemini-cli'], autoSelected: false, reason: 'model heuristic' };
|
|
70
119
|
}
|
|
120
|
+
if (useModelHeuristic && /^kimi(-|$)/i.test(model)) {
|
|
121
|
+
return { backend: REGISTRY['kimi-cli'], autoSelected: false, reason: 'model heuristic' };
|
|
122
|
+
}
|
|
71
123
|
|
|
72
124
|
// No silent auto-fallback to any CLI backend. Sending arbitrary text to a
|
|
73
125
|
// coding agent is a higher-trust action than calling a plain completion
|
|
@@ -76,7 +128,7 @@ export function selectBackend({ name, model } = {}) {
|
|
|
76
128
|
return { backend: REGISTRY['openai-http'], autoSelected: false, reason: 'default' };
|
|
77
129
|
}
|
|
78
130
|
|
|
79
|
-
export function selectBackendChain({ name, model } = {}) {
|
|
131
|
+
export function selectBackendChain({ name, model, modelSource } = {}) {
|
|
80
132
|
if (name) {
|
|
81
133
|
const names = String(name)
|
|
82
134
|
.split(',')
|
|
@@ -96,7 +148,7 @@ export function selectBackendChain({ name, model } = {}) {
|
|
|
96
148
|
};
|
|
97
149
|
}
|
|
98
150
|
|
|
99
|
-
const selected = selectBackend({ model });
|
|
151
|
+
const selected = selectBackend({ model, modelSource });
|
|
100
152
|
return {
|
|
101
153
|
backends: [selected.backend],
|
|
102
154
|
autoSelected: selected.autoSelected,
|
|
@@ -110,27 +162,50 @@ export async function invokeBackendChain({
|
|
|
110
162
|
apiKey,
|
|
111
163
|
baseURL,
|
|
112
164
|
model,
|
|
165
|
+
modelSource,
|
|
113
166
|
signal,
|
|
114
167
|
timeout = DEFAULT_BACKEND_TIMEOUT_MS,
|
|
168
|
+
maxConcurrency,
|
|
169
|
+
maxRetries,
|
|
115
170
|
temperature,
|
|
116
171
|
seed,
|
|
117
172
|
onResponse,
|
|
118
|
-
cache,
|
|
119
173
|
logger,
|
|
120
174
|
}) {
|
|
121
175
|
if (!Array.isArray(backends) || backends.length === 0) {
|
|
122
176
|
throw inputError(
|
|
123
177
|
'no backend selected',
|
|
124
178
|
'patina could not resolve a backend to run.',
|
|
125
|
-
'Pass --backend openai-http, codex-cli, claude-cli, or
|
|
179
|
+
'Pass --backend openai-http, codex-cli, claude-cli, gemini-cli, or kimi-cli.'
|
|
126
180
|
);
|
|
127
181
|
}
|
|
128
182
|
|
|
129
183
|
let lastError = null;
|
|
130
184
|
for (let attemptIndex = 0; attemptIndex < backends.length; attemptIndex++) {
|
|
131
185
|
const backend = backends[attemptIndex];
|
|
186
|
+
const effectiveMaxConcurrency = resolveBackendMaxConcurrency(backend.name, maxConcurrency);
|
|
187
|
+
const effectiveMaxRetries = resolveBackendMaxRetries(backend.name, maxRetries);
|
|
132
188
|
try {
|
|
133
|
-
return await
|
|
189
|
+
return await withBackendConcurrencySlot({
|
|
190
|
+
backendName: backend.name,
|
|
191
|
+
maxConcurrency: effectiveMaxConcurrency,
|
|
192
|
+
signal,
|
|
193
|
+
timeout,
|
|
194
|
+
fn: () => backend.invoke({
|
|
195
|
+
prompt,
|
|
196
|
+
apiKey,
|
|
197
|
+
baseURL,
|
|
198
|
+
model,
|
|
199
|
+
modelSource,
|
|
200
|
+
signal,
|
|
201
|
+
timeout,
|
|
202
|
+
maxRetries: effectiveMaxRetries,
|
|
203
|
+
temperature,
|
|
204
|
+
seed,
|
|
205
|
+
onResponse,
|
|
206
|
+
logger,
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
134
209
|
} catch (err) {
|
|
135
210
|
lastError = err;
|
|
136
211
|
const next = backends[attemptIndex + 1];
|
|
@@ -146,7 +221,7 @@ export async function invokeBackendChain({
|
|
|
146
221
|
throw lastError || new Error('backend fallback chain failed without an error');
|
|
147
222
|
}
|
|
148
223
|
|
|
149
|
-
function resolveBackend(name) {
|
|
224
|
+
export function resolveBackend(name) {
|
|
150
225
|
const backend = REGISTRY[name];
|
|
151
226
|
if (!backend) {
|
|
152
227
|
throw inputError(
|