refactor: prune unused llm contract fields
This commit is contained in:
@@ -159,7 +159,7 @@ stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
|||||||
|
|
||||||
Types: [GenerateOptions](../core-data-structures/core.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
|
Types: [GenerateOptions](../core-data-structures/core.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
|
||||||
|
|
||||||
Source: [`packages/llm/llm/src/index.ts:88`](../../packages/llm/llm/src/index.ts)
|
Source: [`packages/llm/llm/src/index.ts:86`](../../packages/llm/llm/src/index.ts)
|
||||||
|
|
||||||
## `ctx.sandbox` — `SandboxProvider` (abstract seam)
|
## `ctx.sandbox` — `SandboxProvider` (abstract seam)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ The replay seam is the provider-agnostic `llm/stream` waterfall ([packages/llm/l
|
|||||||
|
|
||||||
```
|
```
|
||||||
{ kind: 'chunks', chunks: StreamChunk[] }
|
{ kind: 'chunks', chunks: StreamChunk[] }
|
||||||
| { kind: 'throw', chunks: StreamChunk[], message: string, code: string, status?: number }
|
| { kind: 'throw', chunks: StreamChunk[], message: string, code: string }
|
||||||
| { kind: 'hang' }
|
| { kind: 'hang' }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Status: implemented
|
|||||||
|
|
||||||
Model-driving ACP snapshot scenarios ship both `session.jsonl` and `session.golden.jsonl`. For normal recorded scenarios, `session.jsonl` is the replay fixture harvested from a real run, and the replay test normalizes the newly persisted log and compares it to `session.golden.jsonl`. In the current fixtures, the normalized recorded log and normalized golden are identical for the ordinary recorded scenarios.
|
Model-driving ACP snapshot scenarios ship both `session.jsonl` and `session.golden.jsonl`. For normal recorded scenarios, `session.jsonl` is the replay fixture harvested from a real run, and the replay test normalizes the newly persisted log and compares it to `session.golden.jsonl`. In the current fixtures, the normalized recorded log and normalized golden are identical for the ordinary recorded scenarios.
|
||||||
|
|
||||||
Authored override scenarios (`error-finish`, `cancel`) currently use `replay.override.json` to drive model behavior and keep `session.jsonl` as a minimal dummy fixture, while `session.golden.jsonl` holds the expected persisted log. The override file is a JSON array of `ReplayEntry` objects: `{ "kind": "chunks", "chunks": StreamChunk[] }`, `{ "kind": "throw", "chunks": StreamChunk[], "message": string, "code": string, "status"?: number }`, or `{ "kind": "hang" }`. That split is also unnecessary: when an override sidecar exists, `llm-replay` replaces the derived script and does not need `session.jsonl` for model chunks, so `session.jsonl` can still be the expected session-log artifact for the scenario.
|
Authored override scenarios (`error-finish`, `cancel`) currently use `replay.override.json` to drive model behavior and keep `session.jsonl` as a minimal dummy fixture, while `session.golden.jsonl` holds the expected persisted log. The override file is a JSON array of `ReplayEntry` objects: `{ "kind": "chunks", "chunks": StreamChunk[] }`, `{ "kind": "throw", "chunks": StreamChunk[], "message": string, "code": string }`, or `{ "kind": "hang" }`. That split is also unnecessary: when an override sidecar exists, `llm-replay` replaces the derived script and does not need `session.jsonl` for model chunks, so `session.jsonl` can still be the expected session-log artifact for the scenario.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
{ "kind": "throw", "chunks": [], "message": "simulated provider error (HTTP 401)", "code": "AUTH", "status": 401 }
|
{ "kind": "throw", "chunks": [], "message": "simulated provider error (HTTP 401)", "code": "AUTH" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ export class DeepSeekAdapter extends LlmAdapter {
|
|||||||
const parsed = await response.json() as WireError
|
const parsed = await response.json() as WireError
|
||||||
if (parsed.error?.message) message = parsed.error.message
|
if (parsed.error?.message) message = parsed.error.message
|
||||||
} catch {
|
} catch {
|
||||||
// Paranoid by design: `code` and the HTTP status are ALREADY captured
|
// Paranoid by design: the stable `code` and status-line message are
|
||||||
// above (and passed to LlmError below), so the only thing this `try`
|
// already captured above, so the only thing this `try` can add is a
|
||||||
// can add is a richer provider-supplied message. A malformed, empty,
|
// richer provider-supplied message. A malformed, empty,
|
||||||
// or non-JSON error body is a normal thing for gateways/proxies to
|
// or non-JSON error body is a normal thing for gateways/proxies to
|
||||||
// return on a 5xx/429 — swallowing the parse failure keeps the usable
|
// return on a 5xx/429 — swallowing the parse failure keeps the usable
|
||||||
// status-line message instead of letting a JSON.parse throw mask the
|
// status-line message instead of letting a JSON.parse throw mask the
|
||||||
@@ -89,7 +89,7 @@ export class DeepSeekAdapter extends LlmAdapter {
|
|||||||
// is the sole statement, and any non-parse failure (e.g. body already
|
// is the sole statement, and any non-parse failure (e.g. body already
|
||||||
// consumed) is equally non-actionable here.
|
// consumed) is equally non-actionable here.
|
||||||
}
|
}
|
||||||
throw new LlmError(message, code, response.status)
|
throw new LlmError(message, code)
|
||||||
}
|
}
|
||||||
if (!response.body) {
|
if (!response.body) {
|
||||||
throw new LlmError('DeepSeek API returned no response body', 'EMPTY_RESPONSE')
|
throw new LlmError('DeepSeek API returned no response body', 'EMPTY_RESPONSE')
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ describe('DeepSeekAdapter against a mock server', () => {
|
|||||||
status,
|
status,
|
||||||
body: JSON.stringify({ error: { message: `failed with ${status}`, type: 't', code: 'c' } }),
|
body: JSON.stringify({ error: { message: `failed with ${status}`, type: 't', code: 'c' } }),
|
||||||
}
|
}
|
||||||
const server = await mockServer([behavior, behavior, behavior])
|
const server = await mockServer([behavior, behavior])
|
||||||
const ctx = await harness(server.url)
|
const ctx = await harness(server.url)
|
||||||
await expect(assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] }))
|
await expect(assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] }))
|
||||||
.rejects.toThrow(`failed with ${status}`)
|
.rejects.toThrow(`failed with ${status}`)
|
||||||
@@ -166,11 +166,6 @@ describe('DeepSeekAdapter against a mock server', () => {
|
|||||||
assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })
|
assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })
|
||||||
.catch((error: unknown) => (error as LlmError).code),
|
.catch((error: unknown) => (error as LlmError).code),
|
||||||
).resolves.toBe(code)
|
).resolves.toBe(code)
|
||||||
// The numeric HTTP status is carried on the error for explicit handling.
|
|
||||||
await expect(
|
|
||||||
assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })
|
|
||||||
.catch((error: unknown) => (error as LlmError).status),
|
|
||||||
).resolves.toBe(status)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the status-line message for JSON error bodies without a message', async () => {
|
it('keeps the status-line message for JSON error bodies without a message', async () => {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Every product adapter must identify the application on every provider HTTP reque
|
|||||||
- `LlmAdapter` — abstract base class for provider adapters. The only required method is `stream()`.
|
- `LlmAdapter` — abstract base class for provider adapters. The only required method is `stream()`.
|
||||||
- `BlockAssembler` — incrementally assembles raw chunks into complete content blocks and an assistant message. The agent loop feeds it raw chunks (logging them for replay) while reading the assembled blocks/message for history.
|
- `BlockAssembler` — incrementally assembles raw chunks into complete content blocks and an assistant message. The agent loop feeds it raw chunks (logging them for replay) while reading the assembled blocks/message for history.
|
||||||
- `HarnessError` — base class for the harness error taxonomy: a stable `code` string (distinct from the human `message`) plus `cause` chaining. Lives here, in the leaf package every other imports, so a single base is shared without a new dependency edge. Per-package errors (`LlmError`, `ToolArgsError`, `InvariantError`, …) extend it. `isHarnessError(value)` narrows at seams.
|
- `HarnessError` — base class for the harness error taxonomy: a stable `code` string (distinct from the human `message`) plus `cause` chaining. Lives here, in the leaf package every other imports, so a single base is shared without a new dependency edge. Per-package errors (`LlmError`, `ToolArgsError`, `InvariantError`, …) extend it. `isHarnessError(value)` narrows at seams.
|
||||||
- `LlmError` — extends `HarnessError`; `code` string (`NO_ADAPTER`, `DUPLICATE_ADAPTER`, and adapter codes like `AUTH`/`RATE_LIMIT`) plus an optional numeric `status` when the failure came from a non-2xx provider response.
|
- `LlmError` — extends `HarnessError`; its stable `code` string (`NO_ADAPTER`, `DUPLICATE_ADAPTER`, and adapter codes like `AUTH`/`RATE_LIMIT`) is the programmatic failure contract.
|
||||||
|
|
||||||
### Real adapters
|
### Real adapters
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,10 @@ export class BlockAssembler {
|
|||||||
private _finish: FinishReason | undefined
|
private _finish: FinishReason | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feed one chunk. Returns the completed block when the chunk closes one
|
* Feed one chunk into the assembly state.
|
||||||
* (an explicit `block-end`), otherwise undefined.
|
|
||||||
* @param chunk - the next raw chunk, in stream order.
|
* @param chunk - the next raw chunk, in stream order.
|
||||||
* @returns the authoritative block from the first `block-end` at its index; undefined for every other chunk.
|
|
||||||
*/
|
*/
|
||||||
push(chunk: StreamChunk): ContentBlock | undefined {
|
push(chunk: StreamChunk): void {
|
||||||
switch (chunk.type) {
|
switch (chunk.type) {
|
||||||
case 'block-start': {
|
case 'block-start': {
|
||||||
if (!this.partials.has(chunk.index)) {
|
if (!this.partials.has(chunk.index)) {
|
||||||
@@ -79,7 +77,7 @@ export class BlockAssembler {
|
|||||||
// re-close could rewrite a block already flushed downstream.
|
// re-close could rewrite a block already flushed downstream.
|
||||||
if (partial.block) return
|
if (partial.block) return
|
||||||
partial.block = chunk.block
|
partial.block = chunk.block
|
||||||
return chunk.block
|
return
|
||||||
}
|
}
|
||||||
case 'usage': {
|
case 'usage': {
|
||||||
this._usage = chunk.usage
|
this._usage = chunk.usage
|
||||||
|
|||||||
@@ -42,12 +42,10 @@ declare module 'cordis' {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Typed error for LLM-related failures. Extends {@link HarnessError}, so the
|
* Typed error for LLM-related failures. Extends {@link HarnessError}, so the
|
||||||
* `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy;
|
* `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
|
||||||
* `status` carries the HTTP status when the error originated from a non-2xx
|
|
||||||
* provider response (absent for protocol/usage errors that have no HTTP status).
|
|
||||||
*/
|
*/
|
||||||
export class LlmError extends HarnessError {
|
export class LlmError extends HarnessError {
|
||||||
constructor(message: string, code: string, public status?: number, options?: ErrorOptions) {
|
constructor(message: string, code: string, options?: ErrorOptions) {
|
||||||
super(message, code, options)
|
super(message, code, options)
|
||||||
this.name = 'LlmError'
|
this.name = 'LlmError'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ describe('BlockAssembler', () => {
|
|||||||
expect(assembler.message().role).toBe('assistant')
|
expect(assembler.message().role).toBe('assistant')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns the completed block from push() on block-end', () => {
|
it('records the completed block from block-end', () => {
|
||||||
const assembler = new BlockAssembler()
|
const assembler = new BlockAssembler()
|
||||||
expect(assembler.push({ type: 'block-start', index: 0, blockType: 'text' })).toBeUndefined()
|
assembler.push({ type: 'block-start', index: 0, blockType: 'text' })
|
||||||
expect(assembler.push({ type: 'text-delta', index: 0, text: 'hi' })).toBeUndefined()
|
assembler.push({ type: 'text-delta', index: 0, text: 'hi' })
|
||||||
const block = assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } })
|
assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } })
|
||||||
expect(block).toEqual({ type: 'text', text: 'hi' })
|
expect(assembler.blocks()).toEqual([{ type: 'text', text: 'hi' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tolerates deltas without explicit block-start/end', () => {
|
it('tolerates deltas without explicit block-start/end', () => {
|
||||||
@@ -57,8 +57,8 @@ describe('BlockAssembler', () => {
|
|||||||
// push a delta first to guarantee the partial exists
|
// push a delta first to guarantee the partial exists
|
||||||
assembler.push({ type: 'text-delta', index: 0, text: 'hi' })
|
assembler.push({ type: 'text-delta', index: 0, text: 'hi' })
|
||||||
// block-end's ensure() must find the existing partial (the second branch path)
|
// block-end's ensure() must find the existing partial (the second branch path)
|
||||||
const block = assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } })
|
assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } })
|
||||||
expect(block).toEqual({ type: 'text', text: 'hi' })
|
expect(assembler.blocks()).toEqual([{ type: 'text', text: 'hi' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws from assemble() when a partial has an unhandled blockType', () => {
|
it('throws from assemble() when a partial has an unhandled blockType', () => {
|
||||||
@@ -130,7 +130,7 @@ describe('assertNever', () => {
|
|||||||
|
|
||||||
it('BlockAssembler.push rejects chunks outside the closed StreamChunk union', () => {
|
it('BlockAssembler.push rejects chunks outside the closed StreamChunk union', () => {
|
||||||
const assembler = new BlockAssembler()
|
const assembler = new BlockAssembler()
|
||||||
expect(() => assembler.push({ type: 'rogue-chunk' } as unknown as StreamChunk))
|
expect(() => { assembler.push({ type: 'rogue-chunk' } as unknown as StreamChunk) })
|
||||||
.toThrow('unreachable variant in BlockAssembler.push')
|
.toThrow('unreachable variant in BlockAssembler.push')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -140,32 +140,13 @@ describe('BlockAssembler regressions (property-test findings)', () => {
|
|||||||
// Found by fast-check (the property-testing RFC): two block-ends at the same index made the
|
// Found by fast-check (the property-testing RFC): two block-ends at the same index made the
|
||||||
// streamed prefix (first block) disagree with final blocks() (second
|
// streamed prefix (first block) disagree with final blocks() (second
|
||||||
// block). The first close must win — same straggler rule as post-close
|
// block). The first close must win — same straggler rule as post-close
|
||||||
// deltas — so the prefix returned incrementally by push() and the final
|
// deltas — so later chunks cannot rewrite the completed block.
|
||||||
// blocks() stay identical.
|
|
||||||
const chunks: StreamChunk[] = [
|
const chunks: StreamChunk[] = [
|
||||||
{ type: 'block-end', index: 0, block: { type: 'reasoning', text: 'first' } },
|
{ type: 'block-end', index: 0, block: { type: 'reasoning', text: 'first' } },
|
||||||
{ type: 'block-end', index: 0, block: { type: 'text', text: 'second' } },
|
{ type: 'block-end', index: 0, block: { type: 'text', text: 'second' } },
|
||||||
]
|
]
|
||||||
const streaming = new BlockAssembler()
|
const assembler = new BlockAssembler()
|
||||||
const closed = []
|
for (const chunk of chunks) assembler.push(chunk)
|
||||||
for (const chunk of chunks) {
|
expect(assembler.blocks()).toEqual([{ type: 'reasoning', text: 'first' }])
|
||||||
const block = streaming.push(chunk)
|
|
||||||
if (block) closed.push(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
const oneShot = new BlockAssembler()
|
|
||||||
for (const chunk of chunks) oneShot.push(chunk)
|
|
||||||
|
|
||||||
expect(closed).toEqual([{ type: 'reasoning', text: 'first' }])
|
|
||||||
expect(oneShot.blocks()).toEqual([{ type: 'reasoning', text: 'first' }])
|
|
||||||
expect(closed).toEqual(oneShot.blocks())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('push returns undefined for a duplicate block-end (it closed nothing)', () => {
|
|
||||||
const a = new BlockAssembler()
|
|
||||||
expect(a.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'x' } }))
|
|
||||||
.toEqual({ type: 'text', text: 'x' })
|
|
||||||
expect(a.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'y' } }))
|
|
||||||
.toBeUndefined()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -79,11 +79,12 @@ describe('LlmService', () => {
|
|||||||
|
|
||||||
it('LlmError extends the shared HarnessError base', async () => {
|
it('LlmError extends the shared HarnessError base', async () => {
|
||||||
const { HarnessError, isHarnessError } = await import('@deepseek-ai/dsh-llm')
|
const { HarnessError, isHarnessError } = await import('@deepseek-ai/dsh-llm')
|
||||||
const err = new LlmError('boom', 'AUTH', 401)
|
const cause = new Error('root cause')
|
||||||
|
const err = new LlmError('boom', 'AUTH', { cause })
|
||||||
expect(err).toBeInstanceOf(HarnessError)
|
expect(err).toBeInstanceOf(HarnessError)
|
||||||
expect(isHarnessError(err)).toBe(true)
|
expect(isHarnessError(err)).toBe(true)
|
||||||
expect(err.code).toBe('AUTH')
|
expect(err.code).toBe('AUTH')
|
||||||
expect(err.status).toBe(401)
|
expect(err.cause).toBe(cause)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('HarnessError carries a code, names itself by subclass, and chains cause', async () => {
|
it('HarnessError carries a code, names itself by subclass, and chains cause', async () => {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ import { LlmError, assertNever } from '@deepseek-ai/dsh-llm'
|
|||||||
*/
|
*/
|
||||||
export type ReplayEntry =
|
export type ReplayEntry =
|
||||||
| { kind: 'chunks'; chunks: StreamChunk[] }
|
| { kind: 'chunks'; chunks: StreamChunk[] }
|
||||||
| { kind: 'throw'; chunks: StreamChunk[]; message: string; code: string; status?: number }
|
| { kind: 'throw'; chunks: StreamChunk[]; message: string; code: string }
|
||||||
| { kind: 'hang' }
|
| { kind: 'hang' }
|
||||||
|
|
||||||
/** Resolved plugin configuration. */
|
/** Resolved plugin configuration. */
|
||||||
@@ -324,7 +324,7 @@ async function* replayEntry(entry: ReplayEntry, signal: AbortSignal | undefined)
|
|||||||
if (signal?.aborted) throw new Error('aborted')
|
if (signal?.aborted) throw new Error('aborted')
|
||||||
yield chunk
|
yield chunk
|
||||||
}
|
}
|
||||||
throw new LlmError(entry.message, entry.code, entry.status)
|
throw new LlmError(entry.message, entry.code)
|
||||||
case 'hang':
|
case 'hang':
|
||||||
// Replay a stream that stalls until cancelled (mirrors MockAdapter): one
|
// Replay a stream that stalls until cancelled (mirrors MockAdapter): one
|
||||||
// chunk, then wait for abort and surface it as the consumer expects.
|
// chunk, then wait for abort and surface it as the consumer expects.
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ describe('loadReplayScript', () => {
|
|||||||
it('uses the sidecar override when present, ignoring the JSONL', () => {
|
it('uses the sidecar override when present, ignoring the JSONL', () => {
|
||||||
writeFileSync(file, sessionJsonl([]), 'utf8')
|
writeFileSync(file, sessionJsonl([]), 'utf8')
|
||||||
const overrideFile = join(dir, 'replay.override.json')
|
const overrideFile = join(dir, 'replay.override.json')
|
||||||
const override: ReplayEntry[] = [{ kind: 'throw', chunks: [], message: '401', code: 'AUTH', status: 401 }]
|
const override: ReplayEntry[] = [{ kind: 'throw', chunks: [], message: '401', code: 'AUTH' }]
|
||||||
writeFileSync(overrideFile, JSON.stringify(override), 'utf8')
|
writeFileSync(overrideFile, JSON.stringify(override), 'utf8')
|
||||||
expect(loadReplayScript({ file, overrideFile })).toEqual(override)
|
expect(loadReplayScript({ file, overrideFile })).toEqual(override)
|
||||||
})
|
})
|
||||||
@@ -231,12 +231,12 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
|||||||
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(second)
|
expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(second)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('replays a sidecar throw-entry as an LlmError with code/status, after its prefix chunks', async () => {
|
it('replays a sidecar throw-entry as an LlmError with its stable code, after its prefix chunks', async () => {
|
||||||
writeFileSync(file, sessionJsonl([]), 'utf8')
|
writeFileSync(file, sessionJsonl([]), 'utf8')
|
||||||
const overrideFile = join(dir, 'replay.override.json')
|
const overrideFile = join(dir, 'replay.override.json')
|
||||||
const partial: StreamChunk[] = [{ type: 'block-start', index: 0, blockType: 'text' }]
|
const partial: StreamChunk[] = [{ type: 'block-start', index: 0, blockType: 'text' }]
|
||||||
writeFileSync(overrideFile, JSON.stringify([
|
writeFileSync(overrideFile, JSON.stringify([
|
||||||
{ kind: 'throw', chunks: partial, message: 'unauthorized', code: 'AUTH', status: 401 },
|
{ kind: 'throw', chunks: partial, message: 'unauthorized', code: 'AUTH' },
|
||||||
]), 'utf8')
|
]), 'utf8')
|
||||||
const ctx = new Context()
|
const ctx = new Context()
|
||||||
await ctx.plugin(LlmService)
|
await ctx.plugin(LlmService)
|
||||||
@@ -245,7 +245,7 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
|||||||
const seen: StreamChunk[] = []
|
const seen: StreamChunk[] = []
|
||||||
await expect((async () => {
|
await expect((async () => {
|
||||||
for await (const c of ctx.llm.stream({ model: 'm', messages: [] })) seen.push(c)
|
for await (const c of ctx.llm.stream({ model: 'm', messages: [] })) seen.push(c)
|
||||||
})()).rejects.toMatchObject({ message: 'unauthorized', code: 'AUTH', status: 401 })
|
})()).rejects.toMatchObject({ message: 'unauthorized', code: 'AUTH' })
|
||||||
expect(seen).toEqual(partial)
|
expect(seen).toEqual(partial)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ describe('installLlmReplay (through the real waterfall)', () => {
|
|||||||
const overrideFile = join(dir, 'replay.override.json')
|
const overrideFile = join(dir, 'replay.override.json')
|
||||||
const partial: StreamChunk[] = [{ type: 'block-start', index: 0, blockType: 'text' }]
|
const partial: StreamChunk[] = [{ type: 'block-start', index: 0, blockType: 'text' }]
|
||||||
writeFileSync(overrideFile, JSON.stringify([
|
writeFileSync(overrideFile, JSON.stringify([
|
||||||
{ kind: 'throw', chunks: partial, message: 'unauthorized', code: 'AUTH', status: 401 },
|
{ kind: 'throw', chunks: partial, message: 'unauthorized', code: 'AUTH' },
|
||||||
]), 'utf8')
|
]), 'utf8')
|
||||||
const ctx = new Context()
|
const ctx = new Context()
|
||||||
await ctx.plugin(LlmService)
|
await ctx.plugin(LlmService)
|
||||||
|
|||||||
Reference in New Issue
Block a user