@juspay/neurolink 12.20.2 → 12.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -2
- package/dist/browser/neurolink.min.js +373 -370
- package/dist/cli/commands/decide.d.ts +22 -0
- package/dist/cli/commands/decide.js +287 -0
- package/dist/cli/parser.js +6 -0
- package/dist/neurolink.js +8 -0
- package/dist/proxy/codexAccountUsage.js +15 -0
- package/dist/proxy/codexFallback.js +15 -1
- package/dist/proxy/vertexAnthropicFallback.d.ts +18 -0
- package/dist/proxy/vertexAnthropicFallback.js +305 -12
- package/dist/routing/classifierStrategies.js +7 -0
- package/dist/server/routes/codexProxyRoutes.js +32 -12
- package/dist/types/classifierRouter.d.ts +4 -0
- package/dist/types/cli.d.ts +12 -0
- package/dist/types/codex.d.ts +3 -0
- package/dist/types/proxy.d.ts +1 -1
- package/dist/types/proxyClient.d.ts +4 -1
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +27 -2
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* NeuroLink CLI Decide Command
|
|
4
|
+
*
|
|
5
|
+
* Calls `NeuroLink.decide()` — the third inference type, alongside `generate`
|
|
6
|
+
* and `stream`. Takes a `state` plus a map of named typed questions and
|
|
7
|
+
* prints one calibrated answer per question. See src/lib/types/decision.ts
|
|
8
|
+
* for the full vocabulary.
|
|
9
|
+
*
|
|
10
|
+
* Credentials are env-only here, exactly as every other CLI command: the
|
|
11
|
+
* decision provider (TypeSafe) reads `TYPESAFE_API_KEY` itself.
|
|
12
|
+
*/
|
|
13
|
+
import type { CommandModule } from "yargs";
|
|
14
|
+
import type { CliDecideArgs } from "../../types/index.js";
|
|
15
|
+
export declare const decideCommand: CommandModule<object, CliDecideArgs>;
|
|
16
|
+
/**
|
|
17
|
+
* Create decide command factory for CLICommandFactory-style registration,
|
|
18
|
+
* matching the EvaluateCommandFactory pattern.
|
|
19
|
+
*/
|
|
20
|
+
export declare class DecideCommandFactory {
|
|
21
|
+
static createDecideCommand(): CommandModule;
|
|
22
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* NeuroLink CLI Decide Command
|
|
4
|
+
*
|
|
5
|
+
* Calls `NeuroLink.decide()` — the third inference type, alongside `generate`
|
|
6
|
+
* and `stream`. Takes a `state` plus a map of named typed questions and
|
|
7
|
+
* prints one calibrated answer per question. See src/lib/types/decision.ts
|
|
8
|
+
* for the full vocabulary.
|
|
9
|
+
*
|
|
10
|
+
* Credentials are env-only here, exactly as every other CLI command: the
|
|
11
|
+
* decision provider (TypeSafe) reads `TYPESAFE_API_KEY` itself.
|
|
12
|
+
*/
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import ora from "ora";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import { NeuroLink } from "../../neurolink.js";
|
|
17
|
+
import { readDecisionChoice } from "../../utils/decisionAnswers.js";
|
|
18
|
+
import { calculateCost, hasPricing } from "../../utils/pricing.js";
|
|
19
|
+
import { logger } from "../../utils/logger.js";
|
|
20
|
+
const VALID_QUESTION_TYPES = new Set(["boolean", "choice", "score"]);
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Validates that parsed JSON is a non-empty map of `{ type, ... }` question
|
|
26
|
+
* objects. Runs before any provider work — a malformed `--questions` payload
|
|
27
|
+
* must never reach the network.
|
|
28
|
+
*/
|
|
29
|
+
function isDecisionQuestionMap(value) {
|
|
30
|
+
if (!isRecord(value)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const ids = Object.keys(value);
|
|
34
|
+
return (ids.length > 0 &&
|
|
35
|
+
ids.every((id) => {
|
|
36
|
+
const question = value[id];
|
|
37
|
+
return (isRecord(question) &&
|
|
38
|
+
typeof question.type === "string" &&
|
|
39
|
+
VALID_QUESTION_TYPES.has(question.type));
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/** `state` is the raw text, or its parsed JSON when that JSON is an object or array. */
|
|
43
|
+
function parseState(raw) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (isRecord(parsed) || Array.isArray(parsed)) {
|
|
47
|
+
// Single assertion (CLAUDE.md rule 14 allows this) — `isRecord`/
|
|
48
|
+
// `Array.isArray` narrow `unknown` down to a structural shape TS can't
|
|
49
|
+
// itself equate with the recursive `DecisionInput` union.
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Not JSON — treat as plain text.
|
|
55
|
+
}
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
function isDecisionErrorCause(value) {
|
|
59
|
+
return (isRecord(value) &&
|
|
60
|
+
typeof value.kind === "string" &&
|
|
61
|
+
typeof value.message === "string");
|
|
62
|
+
}
|
|
63
|
+
const ERROR_KIND_MESSAGES = {
|
|
64
|
+
authentication: "Authentication failed — check the decision provider's API key.",
|
|
65
|
+
invalid_request: "The decision provider rejected the request as invalid.",
|
|
66
|
+
max_tokens_exceeded: "The decision request exceeded the provider's token limit.",
|
|
67
|
+
rate_limit: "The decision provider is rate-limiting requests — try again shortly.",
|
|
68
|
+
overloaded: "The decision provider is overloaded — try again shortly.",
|
|
69
|
+
server: "The decision provider returned a server error.",
|
|
70
|
+
timeout: "The decision request timed out.",
|
|
71
|
+
network: "A network error occurred while calling the decision provider.",
|
|
72
|
+
};
|
|
73
|
+
/** One clean line describing a `decide()` failure — never a stack trace. */
|
|
74
|
+
function describeDecideError(error) {
|
|
75
|
+
const cause = error instanceof Error
|
|
76
|
+
? error.cause
|
|
77
|
+
: undefined;
|
|
78
|
+
if (isDecisionErrorCause(cause)) {
|
|
79
|
+
const generic = ERROR_KIND_MESSAGES[cause.kind];
|
|
80
|
+
if (!generic) {
|
|
81
|
+
return cause.message;
|
|
82
|
+
}
|
|
83
|
+
// The provider's own text is what says which field or question was
|
|
84
|
+
// rejected, so it is kept alongside the generic line.
|
|
85
|
+
const detail = cause.message.trim();
|
|
86
|
+
return detail && detail !== generic ? `${generic} (${detail})` : generic;
|
|
87
|
+
}
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
if (message.includes("No decision provider is configured")) {
|
|
90
|
+
return "No decision provider is configured. Set TYPESAFE_API_KEY, or AI_GATEWAY_API_KEY for the Vercel AI Gateway route.";
|
|
91
|
+
}
|
|
92
|
+
return message;
|
|
93
|
+
}
|
|
94
|
+
function formatNumber(value) {
|
|
95
|
+
return value.toFixed(2);
|
|
96
|
+
}
|
|
97
|
+
function formatAnswerLine(id, answers) {
|
|
98
|
+
const answer = answers[id];
|
|
99
|
+
if (!answer) {
|
|
100
|
+
return `${id}: (no answer)`;
|
|
101
|
+
}
|
|
102
|
+
switch (answer.type) {
|
|
103
|
+
case "boolean":
|
|
104
|
+
return `${id}: probability ${formatNumber(answer.probability)}`;
|
|
105
|
+
case "choice": {
|
|
106
|
+
const reading = readDecisionChoice(answers, id);
|
|
107
|
+
if (!reading) {
|
|
108
|
+
return `${id}: (unreadable choice answer)`;
|
|
109
|
+
}
|
|
110
|
+
const alternatives = reading.ranked
|
|
111
|
+
.filter((entry) => entry.name !== reading.choice)
|
|
112
|
+
.slice(0, 2)
|
|
113
|
+
.map((entry) => `${entry.name} (${formatNumber(entry.probability)})`)
|
|
114
|
+
.join(", ");
|
|
115
|
+
return (`${id}: choice ${reading.choice}, confidence ${formatNumber(reading.confidence)}` +
|
|
116
|
+
(alternatives ? `, alternatives: ${alternatives}` : ""));
|
|
117
|
+
}
|
|
118
|
+
case "score":
|
|
119
|
+
return `${id}: score ${formatNumber(answer.score)}, confidence ${formatNumber(answer.confidence)}`;
|
|
120
|
+
default:
|
|
121
|
+
return `${id}: (unrecognised answer type)`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function printTextResult(result) {
|
|
125
|
+
for (const id of Object.keys(result.answers)) {
|
|
126
|
+
logger.always(formatAnswerLine(id, result.answers));
|
|
127
|
+
}
|
|
128
|
+
logger.always("");
|
|
129
|
+
logger.always(chalk.gray(`Model: ${result.model}`));
|
|
130
|
+
logger.always(chalk.gray(`Latency: ${result.latencyMs}ms`));
|
|
131
|
+
if (hasPricing(result.provider, result.model)) {
|
|
132
|
+
const cost = calculateCost(result.provider, result.model, {
|
|
133
|
+
input: result.usage.inputTokens,
|
|
134
|
+
output: result.usage.outputTokens,
|
|
135
|
+
total: result.usage.inputTokens + result.usage.outputTokens,
|
|
136
|
+
});
|
|
137
|
+
logger.always(chalk.gray(`Cost: $${cost.toFixed(6)}`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function readQuestionsSource(argv) {
|
|
141
|
+
if (argv.questionsFile) {
|
|
142
|
+
return fs.readFileSync(argv.questionsFile, "utf-8");
|
|
143
|
+
}
|
|
144
|
+
if (!argv.questions) {
|
|
145
|
+
// Unreachable given the exactly-one-of check in the handler; keeps this
|
|
146
|
+
// function sound on its own rather than asserting the type away.
|
|
147
|
+
throw new Error("No questions source provided.");
|
|
148
|
+
}
|
|
149
|
+
return argv.questions;
|
|
150
|
+
}
|
|
151
|
+
function resolveState(argv) {
|
|
152
|
+
if (argv.stateFile) {
|
|
153
|
+
return parseState(fs.readFileSync(argv.stateFile, "utf-8"));
|
|
154
|
+
}
|
|
155
|
+
if (argv.state) {
|
|
156
|
+
return parseState(argv.state);
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
export const decideCommand = {
|
|
161
|
+
command: "decide [state]",
|
|
162
|
+
describe: "Get typed, calibrated judgements from a decision model (no free text)",
|
|
163
|
+
builder: (yargs) => yargs
|
|
164
|
+
.positional("state", {
|
|
165
|
+
type: "string",
|
|
166
|
+
describe: "The content to judge (or use --state-file)",
|
|
167
|
+
})
|
|
168
|
+
.option("state-file", {
|
|
169
|
+
type: "string",
|
|
170
|
+
describe: "Path to a file holding the state (text or JSON)",
|
|
171
|
+
})
|
|
172
|
+
.option("questions", {
|
|
173
|
+
type: "string",
|
|
174
|
+
describe: "Inline JSON map of questions (exactly one of this or --questions-file)",
|
|
175
|
+
})
|
|
176
|
+
.option("questions-file", {
|
|
177
|
+
type: "string",
|
|
178
|
+
describe: "Path to a JSON file holding the questions map",
|
|
179
|
+
})
|
|
180
|
+
.option("provider", {
|
|
181
|
+
type: "string",
|
|
182
|
+
describe: "Decision provider to use",
|
|
183
|
+
})
|
|
184
|
+
.option("model", {
|
|
185
|
+
type: "string",
|
|
186
|
+
describe: "Overrides the provider's configured model for this call",
|
|
187
|
+
})
|
|
188
|
+
.option("timeout", {
|
|
189
|
+
type: "number",
|
|
190
|
+
describe: "Timeout in milliseconds",
|
|
191
|
+
})
|
|
192
|
+
.option("format", {
|
|
193
|
+
type: "string",
|
|
194
|
+
alias: ["f", "output-format"],
|
|
195
|
+
describe: "Output format",
|
|
196
|
+
choices: ["text", "json"],
|
|
197
|
+
default: "text",
|
|
198
|
+
})
|
|
199
|
+
.option("debug", {
|
|
200
|
+
type: "boolean",
|
|
201
|
+
alias: ["v", "verbose"],
|
|
202
|
+
default: false,
|
|
203
|
+
describe: "Enable debug logging (written to stderr when --format json is used)",
|
|
204
|
+
})
|
|
205
|
+
.example('$0 decide "Refund request for a damaged item" --questions \'{"urgent":{"type":"boolean","instructions":"Is this urgent?"}}\'', "Ask a single yes/no question")
|
|
206
|
+
.example("$0 decide --state-file ticket.json --questions-file questions.json --format json", "Read state and questions from files, emit raw JSON"),
|
|
207
|
+
handler: async (argv) => {
|
|
208
|
+
const outputFormat = argv.format ?? "text";
|
|
209
|
+
// --- Validate before any provider work ---------------------------------
|
|
210
|
+
if (!!argv.questions === !!argv.questionsFile) {
|
|
211
|
+
logger.error(chalk.red("Error: pass exactly one of --questions or --questions-file."));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
let questionsRaw;
|
|
215
|
+
try {
|
|
216
|
+
questionsRaw = readQuestionsSource(argv);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
logger.error(chalk.red(`Error: could not read questions — ${error instanceof Error ? error.message : String(error)}`));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
let parsedQuestions;
|
|
223
|
+
try {
|
|
224
|
+
parsedQuestions = JSON.parse(questionsRaw);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
const flag = argv.questionsFile ? "--questions-file" : "--questions";
|
|
228
|
+
logger.error(chalk.red(`Error: ${flag} is not valid JSON.`));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
if (argv.timeout !== undefined &&
|
|
232
|
+
!(Number.isFinite(argv.timeout) && argv.timeout > 0)) {
|
|
233
|
+
logger.error(chalk.red("Error: --timeout must be a positive number of milliseconds."));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
if (!isDecisionQuestionMap(parsedQuestions)) {
|
|
237
|
+
logger.error(chalk.red('Error: questions must be a non-empty object of { type: "boolean" | "choice" | "score", ... }.'));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
let state;
|
|
241
|
+
try {
|
|
242
|
+
state = resolveState(argv);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
logger.error(chalk.red(`Error: could not read state file — ${error instanceof Error ? error.message : String(error)}`));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
if (state === undefined) {
|
|
249
|
+
logger.error(chalk.red("Error: provide state as a positional argument or via --state-file."));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
// --- Provider work -------------------------------------------------
|
|
253
|
+
const spinner = outputFormat === "json" ? null : ora("Deciding...").start();
|
|
254
|
+
try {
|
|
255
|
+
const neurolink = new NeuroLink();
|
|
256
|
+
const result = await neurolink.decide({
|
|
257
|
+
state,
|
|
258
|
+
questions: parsedQuestions,
|
|
259
|
+
provider: argv.provider,
|
|
260
|
+
model: argv.model,
|
|
261
|
+
timeoutMs: argv.timeout,
|
|
262
|
+
});
|
|
263
|
+
spinner?.succeed("Decision complete");
|
|
264
|
+
if (outputFormat === "json") {
|
|
265
|
+
logger.always(JSON.stringify(result, null, 2));
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
printTextResult(result);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
spinner?.fail("Decision failed");
|
|
273
|
+
logger.error(chalk.red(`Error: ${describeDecideError(error)}`));
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Create decide command factory for CLICommandFactory-style registration,
|
|
280
|
+
* matching the EvaluateCommandFactory pattern.
|
|
281
|
+
*/
|
|
282
|
+
export class DecideCommandFactory {
|
|
283
|
+
static createDecideCommand() {
|
|
284
|
+
return decideCommand;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=decide.js.map
|
package/dist/cli/parser.js
CHANGED
|
@@ -29,6 +29,7 @@ import { proxyPeerCommand } from "./commands/proxyPeer.js";
|
|
|
29
29
|
import { proxyExposeCommand } from "./commands/proxyExpose.js";
|
|
30
30
|
import { proxyReplayCommand } from "./commands/proxyReplay.js";
|
|
31
31
|
import { EvaluateCommandFactory } from "./commands/evaluate.js";
|
|
32
|
+
import { DecideCommandFactory } from "./commands/decide.js";
|
|
32
33
|
import { TaskCommandFactory } from "./commands/task.js";
|
|
33
34
|
import { AutoresearchCommandFactory } from "./commands/autoresearch.js";
|
|
34
35
|
import { voiceServerCommand } from "./commands/voiceServer.js";
|
|
@@ -72,6 +73,9 @@ export function initializeCliParser() {
|
|
|
72
73
|
// Always set to false when debug is not enabled (including when not provided)
|
|
73
74
|
process.env.NEUROLINK_DEBUG = "false";
|
|
74
75
|
}
|
|
76
|
+
// With --format json, stdout carries the payload; a debug or info
|
|
77
|
+
// line there would make it unparseable, so diagnostics go to stderr.
|
|
78
|
+
logger.setDiagnosticsToStderr(argv.format === "json");
|
|
75
79
|
// Keep existing quiet middleware
|
|
76
80
|
if (process.env.NEUROLINK_QUIET === "true" &&
|
|
77
81
|
typeof argv.quiet === "undefined") {
|
|
@@ -164,6 +168,8 @@ export function initializeCliParser() {
|
|
|
164
168
|
.command(UsageCommandFactory.createUsageCommands())
|
|
165
169
|
// Stream Text Command - Using CLICommandFactory
|
|
166
170
|
.command(CLICommandFactory.createStreamCommand())
|
|
171
|
+
// Decide Command - typed, calibrated judgements (no free text)
|
|
172
|
+
.command(DecideCommandFactory.createDecideCommand())
|
|
167
173
|
// Batch Processing Command - Using CLICommandFactory
|
|
168
174
|
.command(CLICommandFactory.createBatchCommand())
|
|
169
175
|
// Provider Command Group - Using CLICommandFactory
|
package/dist/neurolink.js
CHANGED
|
@@ -4019,6 +4019,12 @@ Current user's request: ${currentInput}`;
|
|
|
4019
4019
|
const sessionId = typeof opt.context?.sessionId === "string"
|
|
4020
4020
|
? opt.context.sessionId
|
|
4021
4021
|
: undefined;
|
|
4022
|
+
// Routing runs before conversation memory is loaded, so only the history
|
|
4023
|
+
// the caller passed in is countable here.
|
|
4024
|
+
const sessionBound = sessionId !== undefined;
|
|
4025
|
+
const priorMessageCount = Array.isArray(opt.conversationMessages)
|
|
4026
|
+
? opt.conversationMessages.length
|
|
4027
|
+
: undefined;
|
|
4022
4028
|
try {
|
|
4023
4029
|
const decision = await this.classifierRouter.route({
|
|
4024
4030
|
prompt: promptText,
|
|
@@ -4027,6 +4033,8 @@ Current user's request: ${currentInput}`;
|
|
|
4027
4033
|
requiresVision,
|
|
4028
4034
|
thinkingLevel,
|
|
4029
4035
|
sessionId,
|
|
4036
|
+
sessionBound,
|
|
4037
|
+
priorMessageCount,
|
|
4030
4038
|
});
|
|
4031
4039
|
if (!decision) {
|
|
4032
4040
|
return;
|
|
@@ -11,6 +11,15 @@ import { normalizeAnthropicAccountKey } from "./accountSelection.js";
|
|
|
11
11
|
import { CODEX_USAGE_URL, CODEX_USER_AGENT, decodeCodexAccessToken, resolveCodexAccountId, } from "../auth/codexOAuth.js";
|
|
12
12
|
import { logger } from "../utils/logger.js";
|
|
13
13
|
export const CODEX_ACCOUNT_PREFIX = "codex:";
|
|
14
|
+
/**
|
|
15
|
+
* Native fallback legs (e.g. Claude-on-Vertex) key their usage rows as
|
|
16
|
+
* `vertex/<model>` rather than a provider-qualified `<provider>:<label>`
|
|
17
|
+
* pair — there is no OAuth account behind them, just a served model. They
|
|
18
|
+
* are not served by Codex, so without this prefix they fell through to the
|
|
19
|
+
* generic "other" branch below and lost both their identity and their live
|
|
20
|
+
* status in the CLI's proxy status output.
|
|
21
|
+
*/
|
|
22
|
+
const VERTEX_ACCOUNT_PREFIX = "vertex/";
|
|
14
23
|
/** Keep an account label in the Codex namespace used by its token store. */
|
|
15
24
|
export function normalizeCodexAccountKey(value) {
|
|
16
25
|
const trimmed = value.trim();
|
|
@@ -32,12 +41,18 @@ export function resolveProxyStatusAccountIdentity(label, type, persistedKey) {
|
|
|
32
41
|
if (persistedKey?.startsWith(CODEX_ACCOUNT_PREFIX)) {
|
|
33
42
|
return { provider: "codex", key: persistedKey };
|
|
34
43
|
}
|
|
44
|
+
if (persistedKey?.startsWith(VERTEX_ACCOUNT_PREFIX)) {
|
|
45
|
+
return { provider: "vertex", key: persistedKey };
|
|
46
|
+
}
|
|
35
47
|
if (type === "oauth" || type === "api_key") {
|
|
36
48
|
return { provider: "anthropic", key: normalizeAnthropicAccountKey(label) };
|
|
37
49
|
}
|
|
38
50
|
if (type === "codex-oauth") {
|
|
39
51
|
return { provider: "codex", key: normalizeCodexAccountKey(label) };
|
|
40
52
|
}
|
|
53
|
+
if (type === "vertex") {
|
|
54
|
+
return { provider: "vertex", key: persistedKey ?? label };
|
|
55
|
+
}
|
|
41
56
|
return { provider: "other", key: null };
|
|
42
57
|
}
|
|
43
58
|
/** Enumerate Codex OAuth accounts from the token store for usage refresh. */
|
|
@@ -739,7 +739,21 @@ export async function createCodexFallbackStream(response, model) {
|
|
|
739
739
|
try {
|
|
740
740
|
yield* serializer.start();
|
|
741
741
|
while (true) {
|
|
742
|
-
|
|
742
|
+
let chunk;
|
|
743
|
+
try {
|
|
744
|
+
chunk = await reader.read();
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
// A premature stream close after a complete SSE stream (but before
|
|
748
|
+
// the zero-length terminator) rejects here. If response.completed
|
|
749
|
+
// already landed, treat the close as a normal end-of-stream rather
|
|
750
|
+
// than failing a fully successful turn; otherwise this is genuine
|
|
751
|
+
// truncation and must still fail visibly.
|
|
752
|
+
if (completed) {
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
throw err;
|
|
756
|
+
}
|
|
743
757
|
carry += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
744
758
|
// Search only new bytes plus the boundary overlap. Long tool payloads
|
|
745
759
|
// split over many chunks must not rescan their accumulated prefix.
|
|
@@ -21,6 +21,17 @@ import type { VertexAccessTokenProvider, UsageContext, VertexPassthroughTerminal
|
|
|
21
21
|
* `effort` key, never the sibling settings beside it.
|
|
22
22
|
*/
|
|
23
23
|
export declare function mapVertexOutputConfig(value: unknown): Record<string, unknown> | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Strip what Vertex rejects and pin the API version it requires.
|
|
26
|
+
*
|
|
27
|
+
* Prompt caching constrains every rewrite here. Anthropic caches only up to an
|
|
28
|
+
* explicit `cache_control` marker, at most four per request, over a prefix
|
|
29
|
+
* rendered `tools → system → messages`; anything removed or inserted before a
|
|
30
|
+
* marker invalidates it and everything after. So: never drop a marker, never
|
|
31
|
+
* reorder `system`, and keep each rewrite deterministic so consecutive turns
|
|
32
|
+
* share a prefix. Vertex holds its own cache, apart from the first-party one,
|
|
33
|
+
* so nothing here can disturb first-party hits.
|
|
34
|
+
*/
|
|
24
35
|
export declare function buildVertexAnthropicPayload(body: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
25
36
|
/**
|
|
26
37
|
* Publisher endpoint for a Claude model on Vertex.
|
|
@@ -45,6 +56,13 @@ export declare function buildVertexAnthropicUrl(target: VertexAnthropicPassthrou
|
|
|
45
56
|
* Pass `undefined` to restore the real credential lookup.
|
|
46
57
|
*/
|
|
47
58
|
export declare function setVertexAccessTokenProviderForTests(provider: VertexAccessTokenProvider | undefined): void;
|
|
59
|
+
/**
|
|
60
|
+
* Run once more after a short pause when the first attempt hit a transient
|
|
61
|
+
* network failure: anything the shared proxy classifier retries, plus a DNS
|
|
62
|
+
* blip. One retry absorbs a blip without hiding a real outage: a second
|
|
63
|
+
* failure, or any other error, surfaces unchanged.
|
|
64
|
+
*/
|
|
65
|
+
export declare function withTransientNetworkRetry<T>(run: () => Promise<T>, delayMs?: number): Promise<T>;
|
|
48
66
|
/**
|
|
49
67
|
* Send one Anthropic-shaped request to Vertex and return the upstream response
|
|
50
68
|
* untouched, so the caller can stream its bytes straight to the client.
|