@ultimat3/ai 30.9.0 → 31.1.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/CLAUDE.md +20 -11
- package/README.md +47 -22
- package/package.json +13 -12
- package/src/adapter.ts +21 -0
- package/src/agent-compact.ts +5 -2
- package/src/agent-continue.ts +4 -69
- package/src/agent-def.ts +6 -1
- package/src/agent-session.ts +66 -0
- package/src/agent-system.ts +1 -1
- package/src/agent-tools.ts +123 -0
- package/src/agent-turn.ts +14 -4
- package/src/agent.ts +43 -88
- package/src/budget-store.ts +82 -0
- package/src/budget.ts +7 -78
- package/src/embeddings.ts +1 -1
- package/src/errors.ts +5 -5
- package/src/eval-agent.ts +1 -1
- package/src/gateway-providers.ts +22 -10
- package/src/gateway.ts +20 -21
- package/src/index.ts +6 -71
- package/src/llm-answer.ts +125 -0
- package/src/llm-cache.ts +16 -2
- package/src/llm-def.ts +1 -1
- package/src/llm-instrument.ts +100 -0
- package/src/llm.ts +152 -241
- package/src/openai-provider.ts +2 -1
- package/src/openai-wire-stream.ts +243 -0
- package/src/openai-wire.ts +9 -236
- package/src/pg-vector-sql.ts +30 -2
- package/src/provider.ts +1 -1
- package/src/runtime.ts +83 -38
- package/src/sampling.ts +4 -4
- package/src/scorers.ts +1 -1
- package/src/tools.ts +12 -8
- package/src/vector.ts +3 -1
package/src/llm.ts
CHANGED
|
@@ -23,25 +23,20 @@
|
|
|
23
23
|
|
|
24
24
|
import type { InvokeOptions, StreamAction } from '@ultimat3/action';
|
|
25
25
|
import { action, chunksRead } from '@ultimat3/action';
|
|
26
|
-
import type { Ctx
|
|
27
|
-
import { finiteCount, withSpan } from '@ultimat3/core';
|
|
26
|
+
import type { Ctx } from '@ultimat3/core';
|
|
27
|
+
import { finiteCount, SEMCONV, withSpan } from '@ultimat3/core';
|
|
28
28
|
import type { InferInput, InferOutput, StandardSchemaV1 } from '@ultimat3/schema';
|
|
29
29
|
import { formatIssues, t, validateAsync } from '@ultimat3/schema';
|
|
30
|
-
import { runTotal } from './agent-usage';
|
|
31
30
|
import { LlmTruncatedError } from './answer-errors';
|
|
32
31
|
import type { BudgetLimits } from './budget';
|
|
33
32
|
import { budgetKeysFor, currentBudget, screenedMoney, withBudget } from './budget';
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
LlmOutputInvalidError,
|
|
37
|
-
LlmRefusedError,
|
|
38
|
-
LlmStreamInvalidError,
|
|
39
|
-
} from './errors';
|
|
40
|
-
import type { Gateway } from './gateway';
|
|
33
|
+
import { AiTransportError, LlmOutputInvalidError, LlmRefusedError } from './errors';
|
|
34
|
+
import { accept, answerAttributes, observeCall, streamedAnswer } from './llm-answer';
|
|
41
35
|
import { openCache } from './llm-cache';
|
|
42
36
|
import type { LlmBudget, LlmDef } from './llm-def';
|
|
43
|
-
import
|
|
44
|
-
import {
|
|
37
|
+
import { llmReport } from './llm-instrument';
|
|
38
|
+
import type { LlmStreamChunk } from './llm-stream';
|
|
39
|
+
import { currentLlmSink, llmStream, withLlmSink } from './llm-stream';
|
|
45
40
|
import { modelForCall } from './model-resolve';
|
|
46
41
|
import { moreCapableThan } from './models';
|
|
47
42
|
import type { PromptVars } from './prompt';
|
|
@@ -50,8 +45,7 @@ import type { AiMessage, GenerateRequest, GenerateResult } from './provider';
|
|
|
50
45
|
import { isTruncated } from './provider';
|
|
51
46
|
import { assertNoSecrets } from './redaction';
|
|
52
47
|
import type { Respond } from './respond';
|
|
53
|
-
import {
|
|
54
|
-
import type { RunMeter } from './run-meter';
|
|
48
|
+
import { RESPOND, respondFor } from './respond';
|
|
55
49
|
import { runMeter } from './run-meter';
|
|
56
50
|
import { aiGateway, aiRedactor, installedGateway } from './runtime';
|
|
57
51
|
import type { Sampling } from './sampling';
|
|
@@ -204,239 +198,156 @@ async function generate<
|
|
|
204
198
|
const redacted = rendered !== rawPrompt || system !== prompt.system;
|
|
205
199
|
const promptCache = promptCacheHint(def.promptCache, 'off', args);
|
|
206
200
|
|
|
207
|
-
return withSpan(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
'
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
);
|
|
222
|
-
|
|
201
|
+
return withSpan(
|
|
202
|
+
'ai.llm',
|
|
203
|
+
async (span) => {
|
|
204
|
+
// OpenTelemetry's gen_ai names for what semconv covers (v31); the framework's own facts are
|
|
205
|
+
// `ultimate.llm.*`. The provider, response model and token usage land when the call settles.
|
|
206
|
+
span.setAttributes({
|
|
207
|
+
[SEMCONV.genAiOperationName]: 'chat',
|
|
208
|
+
[SEMCONV.genAiRequestModel]: model,
|
|
209
|
+
'ultimate.llm.prompt': prompt.ref,
|
|
210
|
+
'ultimate.llm.prompt.hash': prompt.hash,
|
|
211
|
+
// Whether the installed redactor changed anything. Recorded because "we redact" is a claim
|
|
212
|
+
// an audit asks evidence for, and a redactor that silently stopped matching looks identical
|
|
213
|
+
// to one that had nothing to remove until this attribute separates them.
|
|
214
|
+
'ultimate.llm.redacted': redacted,
|
|
215
|
+
});
|
|
216
|
+
// Read when the call ends, not here: the gateway may be what registers the model's row.
|
|
217
|
+
const reportSampling = (): void => {
|
|
218
|
+
span.setAttributes(
|
|
219
|
+
ignoredSamplingAttribute(model, sampling, () =>
|
|
220
|
+
installedGateway()?.samplingWires?.(model),
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
};
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
225
|
+
// A cached answer is still data of unknown provenance, so it goes through the schema like
|
|
226
|
+
// any other. One that no longer fits — the schema moved under it — is a miss, not a
|
|
227
|
+
// failure: the model can produce a fresh answer, and refusing would be worse than paying.
|
|
228
|
+
const cache = await openCache({
|
|
229
|
+
cache: def.cache,
|
|
230
|
+
prompt: { ref: prompt.ref, hash: prompt.hash },
|
|
231
|
+
input: args.input,
|
|
232
|
+
ctx: args.ctx,
|
|
233
|
+
rendered,
|
|
234
|
+
});
|
|
235
|
+
const hit = await accept(def.output, await cache?.lookup());
|
|
236
|
+
span.setAttribute('ultimate.llm.cache.hit', hit !== undefined);
|
|
237
|
+
// Every provider call below goes through this meter, so the report sees who answered.
|
|
238
|
+
const report = llmReport(span, model, runMeter());
|
|
239
|
+
const meter = report.meter;
|
|
240
|
+
if (hit !== undefined) {
|
|
241
|
+
reportSampling();
|
|
242
|
+
report.settle({ cacheHit: true });
|
|
243
|
+
return hit.value;
|
|
244
|
+
}
|
|
240
245
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
246
|
+
// No `tools` yet: the `respond` projection belongs to the non-streaming path alone. A tool
|
|
247
|
+
// call is emitted whole, so forcing the answer through one leaves a stream with nothing to
|
|
248
|
+
// deliver until it is already over.
|
|
249
|
+
const base: GenerateRequest = {
|
|
250
|
+
model,
|
|
251
|
+
...(system === undefined ? {} : { system }),
|
|
252
|
+
...(promptCache === undefined ? {} : { promptCache }),
|
|
253
|
+
messages: [{ role: 'user', content: rendered }],
|
|
254
|
+
maxTokens: def.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
255
|
+
...(prompt.effort === undefined ? {} : { effort: prompt.effort }),
|
|
256
|
+
...(prompt.thinking === undefined ? {} : { thinking: prompt.thinking }),
|
|
257
|
+
...sampling,
|
|
258
|
+
// The caller's own signal, forwarded to the transport exactly as `agent()` does — and
|
|
259
|
+
// inherited by `streamedAnswer` from this same `base`. Without it a disconnected caller left
|
|
260
|
+
// the provider call in flight, billed and unread, and the repair turn bought a SECOND one.
|
|
261
|
+
signal: args.ctx.signal,
|
|
262
|
+
};
|
|
263
|
+
const request: GenerateRequest = { ...base, tools: [respond.tool] };
|
|
259
264
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
265
|
+
// A ledger derived from the ambient one, so a per-call budget can only TIGHTEN the actor
|
|
266
|
+
// and org ceilings this call runs inside, never widen them. The gateway reserves against
|
|
267
|
+
// it before the provider is touched — that is where `X_AI_BUDGET_EXCEEDED` comes from.
|
|
268
|
+
// Rooted in the GATEWAY's own ceilings when no scope is open — an empty root ignored them.
|
|
269
|
+
const gateway = aiGateway(name);
|
|
270
|
+
// Keyed on the CALLER, so the gateway's `actor` / `org` ceilings count this call against them.
|
|
271
|
+
const ledger = (currentBudget() ?? gateway.callLedger(budgetKeysFor(args.ctx.actor))).derive(
|
|
272
|
+
limits,
|
|
273
|
+
);
|
|
274
|
+
const sink = currentLlmSink();
|
|
270
275
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return value;
|
|
279
|
-
}
|
|
280
|
-
let messages: readonly AiMessage[] = request.messages;
|
|
281
|
-
let issues = 'no output';
|
|
282
|
-
let rejected: readonly string[] | undefined;
|
|
283
|
-
for (let attempt = 1; attempt <= ATTEMPTS; attempt += 1) {
|
|
284
|
-
const result = await meter.call(gateway.generate({ ...request, messages }));
|
|
285
|
-
span.setAttributes({ 'llm.attempts': attempt, ...answerAttributes(result) });
|
|
286
|
-
await observeCall(def, meter, result, attempt, rejected);
|
|
287
|
-
// Branch on the stop reason BEFORE reading the answer. A refusal carries empty or partial
|
|
288
|
-
// content, so parsing it first reports a schema disagreement — a cause that is wrong, a
|
|
289
|
-
// fix that does not apply, and a repair turn spent buying the same refusal again.
|
|
290
|
-
if (result.stopReason === 'refusal') {
|
|
291
|
-
throw new LlmRefusedError({
|
|
292
|
-
prompt: name,
|
|
293
|
-
model: result.model,
|
|
294
|
-
// The fix names a model the caller can paste, and only ever a MORE capable one:
|
|
295
|
-
// "the first id that differs" answered a refusal on the default model with the next
|
|
296
|
-
// entry down the ladder, which is a retry that cannot succeed.
|
|
297
|
-
alternative: moreCapableThan(result.model, request),
|
|
298
|
-
category: result.stopDetails?.category,
|
|
299
|
-
explanation: result.stopDetails?.explanation,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
// Cut off, so there is no answer to read — and it is read BEFORE the schema for the reason
|
|
303
|
-
// the refusal is: a `respond` call stopped at the ceiling arrives with no arguments, both
|
|
304
|
-
// wires read that as `{}`, and `{}` SATISFIES an `output` whose keys are optional. The
|
|
305
|
-
// ceiling is the same on the next attempt, so the repair turn is a second truncation.
|
|
306
|
-
if (isTruncated(result.stopReason)) {
|
|
307
|
-
throw new LlmTruncatedError({ prompt: name, maxTokens: request.maxTokens });
|
|
276
|
+
// Every provider call goes through the meter, so a failure AFTER one answered — a refusal, a
|
|
277
|
+
// truncation, a schema no repair satisfied — is thrown carrying what it was billed.
|
|
278
|
+
const answer = async (): Promise<InferOutput<TOutput>> => {
|
|
279
|
+
if (sink !== undefined) {
|
|
280
|
+
const value = await streamedAnswer(def, name, gateway, base, sink, span, meter);
|
|
281
|
+
await cache?.remember(value);
|
|
282
|
+
return value;
|
|
308
283
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
284
|
+
let messages: readonly AiMessage[] = request.messages;
|
|
285
|
+
let issues = 'no output';
|
|
286
|
+
let rejected: readonly string[] | undefined;
|
|
287
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt += 1) {
|
|
288
|
+
const result = await meter.call(gateway.generate({ ...request, messages }));
|
|
289
|
+
span.setAttributes({ 'ultimate.llm.attempts': attempt, ...answerAttributes(result) });
|
|
290
|
+
await observeCall(def, meter, result, attempt, rejected);
|
|
291
|
+
// Branch on the stop reason BEFORE reading the answer. A refusal carries empty or partial
|
|
292
|
+
// content, so parsing it first reports a schema disagreement — a cause that is wrong, a
|
|
293
|
+
// fix that does not apply, and a repair turn spent buying the same refusal again.
|
|
294
|
+
if (result.stopReason === 'refusal') {
|
|
295
|
+
throw new LlmRefusedError({
|
|
296
|
+
prompt: name,
|
|
297
|
+
model: result.model,
|
|
298
|
+
// The fix names a model the caller can paste, and only ever a MORE capable one:
|
|
299
|
+
// "the first id that differs" answered a refusal on the default model with the next
|
|
300
|
+
// entry down the ladder, which is a retry that cannot succeed.
|
|
301
|
+
alternative: moreCapableThan(result.model, request),
|
|
302
|
+
category: result.stopDetails?.category,
|
|
303
|
+
explanation: result.stopDetails?.explanation,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
// Cut off, so there is no answer to read — and it is read BEFORE the schema for the reason
|
|
307
|
+
// the refusal is: a `respond` call stopped at the ceiling arrives with no arguments, both
|
|
308
|
+
// wires read that as `{}`, and `{}` SATISFIES an `output` whose keys are optional. The
|
|
309
|
+
// ceiling is the same on the next attempt, so the repair turn is a second truncation.
|
|
310
|
+
if (isTruncated(result.stopReason)) {
|
|
311
|
+
throw new LlmTruncatedError({ prompt: name, maxTokens: request.maxTokens });
|
|
312
|
+
}
|
|
313
|
+
const parsed = await validateAsync(def.output, respond.read(result));
|
|
314
|
+
if (parsed.issues === undefined) {
|
|
315
|
+
await cache?.remember(parsed.value);
|
|
316
|
+
return parsed.value;
|
|
317
|
+
}
|
|
318
|
+
rejected = formatIssues(parsed.issues);
|
|
319
|
+
issues = rejected.join('; ');
|
|
320
|
+
const echo = assistantEcho(result);
|
|
321
|
+
messages =
|
|
322
|
+
echo === undefined
|
|
323
|
+
? [...messages, repair(issues)]
|
|
324
|
+
: [...messages, { role: 'assistant', content: echo }, repair(issues)];
|
|
313
325
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* What one answered turn puts on the span. `llm.provider` is the half the LLM-gateway table
|
|
341
|
-
* called "a fallback is recorded in the span, never silent": fallback in this framework is across
|
|
342
|
-
* PROVIDERS serving one model, never across models, and until the gateway stamped the provider
|
|
343
|
-
* that answered, a fallback was exactly as silent as no fallback at all.
|
|
344
|
-
*/
|
|
345
|
-
export function answerAttributes(result: GenerateResult): SpanAttributes {
|
|
346
|
-
return {
|
|
347
|
-
'llm.stop': result.stopReason,
|
|
348
|
-
'llm.tokens': result.usage.inputTokens + result.usage.outputTokens,
|
|
349
|
-
'llm.cost.minor': result.cost.minor,
|
|
350
|
-
'llm.provider': result.provider ?? 'unknown',
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* One streamed turn, from the same declaration and under the same rules — with one difference
|
|
356
|
-
* that is forced rather than chosen: no repair turn. The tokens are already on the consumer's
|
|
357
|
-
* screen, so a second answer would be two answers to one question; a stream gets one attempt and
|
|
358
|
-
* `X_LLM_STREAM_INVALID` names the non-streaming call as the fix.
|
|
359
|
-
*
|
|
360
|
-
* The stop reason is still read BEFORE the answer, for the reason it always was: a refusal
|
|
361
|
-
* carries empty or partial content and parsing it first reports a schema disagreement that is not
|
|
362
|
-
* one. Truncation is the same call it is on the non-streaming path.
|
|
363
|
-
*/
|
|
364
|
-
async function streamedAnswer<
|
|
365
|
-
TInput extends StandardSchemaV1,
|
|
366
|
-
TOutput extends StandardSchemaV1,
|
|
367
|
-
V extends PromptVars,
|
|
368
|
-
>(
|
|
369
|
-
def: LlmDef<TInput, TOutput, V>,
|
|
370
|
-
name: string,
|
|
371
|
-
gateway: Gateway,
|
|
372
|
-
request: GenerateRequest,
|
|
373
|
-
sink: LlmSink,
|
|
374
|
-
span: Span,
|
|
375
|
-
meter: RunMeter,
|
|
376
|
-
): Promise<InferOutput<TOutput>> {
|
|
377
|
-
const { output } = def;
|
|
378
|
-
const result = await meter.call(streamOneTurn(gateway, request, sink));
|
|
379
|
-
span.setAttributes({ 'llm.attempts': 1, ...answerAttributes(result) });
|
|
380
|
-
await observeCall(def, meter, result, 1, undefined);
|
|
381
|
-
if (result.stopReason === 'refusal') {
|
|
382
|
-
throw new LlmRefusedError({
|
|
383
|
-
prompt: name,
|
|
384
|
-
model: result.model,
|
|
385
|
-
alternative: moreCapableThan(result.model, request),
|
|
386
|
-
category: result.stopDetails?.category,
|
|
387
|
-
explanation: result.stopDetails?.explanation,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
if (isTruncated(result.stopReason)) {
|
|
391
|
-
throw new LlmTruncatedError({ prompt: name, maxTokens: request.maxTokens });
|
|
392
|
-
}
|
|
393
|
-
// Prose, and its JSON parse when it has one. Both, because a stream carries no `respond` tool
|
|
394
|
-
// to tell them apart: `output: t.string` is satisfied by the text itself, and an object schema
|
|
395
|
-
// by what the text parses to — trying only one of the two makes a legal declaration unusable.
|
|
396
|
-
const parsed = await accept(output, parseJsonish(result.text));
|
|
397
|
-
if (parsed !== undefined) return parsed.value;
|
|
398
|
-
const fallback = await validateAsync(output, result.text);
|
|
399
|
-
if (fallback.issues === undefined) return fallback.value;
|
|
400
|
-
throw new LlmStreamInvalidError({
|
|
401
|
-
prompt: name,
|
|
402
|
-
issues: formatIssues(fallback.issues).join('; '),
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/** One provider call, reported to the declaration's `onCall`. Awaited; a throw fails the call. */
|
|
407
|
-
async function observeCall<
|
|
408
|
-
TInput extends StandardSchemaV1,
|
|
409
|
-
TOutput extends StandardSchemaV1,
|
|
410
|
-
V extends PromptVars,
|
|
411
|
-
>(
|
|
412
|
-
def: LlmDef<TInput, TOutput, V>,
|
|
413
|
-
meter: RunMeter,
|
|
414
|
-
result: GenerateResult,
|
|
415
|
-
attempt: number,
|
|
416
|
-
repairOf: readonly string[] | undefined,
|
|
417
|
-
): Promise<void> {
|
|
418
|
-
if (def.onCall === undefined) return;
|
|
419
|
-
await def.onCall({
|
|
420
|
-
attempt,
|
|
421
|
-
model: result.model,
|
|
422
|
-
provider: result.provider,
|
|
423
|
-
stopReason: result.stopReason,
|
|
424
|
-
usage: result.usage,
|
|
425
|
-
cost: result.cost,
|
|
426
|
-
...(result.costExact === undefined ? {} : { costExact: result.costExact }),
|
|
427
|
-
...(repairOf === undefined ? {} : { repairOf }),
|
|
428
|
-
run: runTotal(result.model, undefined, meter.total()),
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/** `undefined` for "does not fit". Wrapped so a legitimately falsy value is still a hit. */
|
|
433
|
-
async function accept<TOutput extends StandardSchemaV1>(
|
|
434
|
-
schema: TOutput,
|
|
435
|
-
value: unknown,
|
|
436
|
-
): Promise<{ readonly value: InferOutput<TOutput> } | undefined> {
|
|
437
|
-
if (value === undefined) return undefined;
|
|
438
|
-
const parsed = await validateAsync(schema, value);
|
|
439
|
-
return parsed.issues === undefined ? { value: parsed.value } : undefined;
|
|
326
|
+
throw new LlmOutputInvalidError({
|
|
327
|
+
prompt: name,
|
|
328
|
+
attempts: ATTEMPTS,
|
|
329
|
+
issues,
|
|
330
|
+
...(rejected === undefined ? {} : { list: rejected }),
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
return withBudget(ledger, () =>
|
|
334
|
+
answer()
|
|
335
|
+
.then(
|
|
336
|
+
(value) => {
|
|
337
|
+
report.settle({ cacheHit: false });
|
|
338
|
+
return value;
|
|
339
|
+
},
|
|
340
|
+
(error: unknown) => {
|
|
341
|
+
const billed = meter.fail(error);
|
|
342
|
+
report.settle({ cacheHit: false, error: billed });
|
|
343
|
+
throw billed;
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
.finally(reportSampling),
|
|
347
|
+
);
|
|
348
|
+
},
|
|
349
|
+
{ kind: 'client' },
|
|
350
|
+
);
|
|
440
351
|
}
|
|
441
352
|
|
|
442
353
|
function repair(issues: string): AiMessage {
|
package/src/openai-provider.ts
CHANGED
|
@@ -18,7 +18,8 @@ import { resolveModel } from './model-resolve';
|
|
|
18
18
|
import type { ModelId } from './models';
|
|
19
19
|
import { chatCompletionBody, sessionHeaders } from './openai-body';
|
|
20
20
|
import type { ChatAnswer } from './openai-wire';
|
|
21
|
-
import {
|
|
21
|
+
import { parseChatCompletion } from './openai-wire';
|
|
22
|
+
import { ChatCompletionStream } from './openai-wire-stream';
|
|
22
23
|
import type { ProviderCacheRow, ProviderPromptCache } from './prompt-cache';
|
|
23
24
|
import { promptCacheRule } from './prompt-cache';
|
|
24
25
|
import { screenCacheRows } from './prompt-cache-rows';
|