Skip to content

Commit 3c24fe3

Browse files
heskewclaude
andcommitted
fix(models): generateStream capability-mismatch records the resolved backend name
Caught by Claude review bot on PR #638: `generateStream`'s catch was hard-coding `undefined` as the backend, so capability-mismatch failures (registry hit, capability check fail) produced `backend: 'unknown'` rows even though the backend WAS found — diverging from `embed` / `generate` which correctly pass the captured `backend` variable. The F2 fix in commit fabef73 covered the registry-miss case but missed the capability-mismatch path on streams specifically. Change: declare `let backend: ModelBackend | undefined` (matches embed/generate) and pass `backend` (not `undefined`) into `#recordFailure`. When `resolveGenerative()` throws (registry miss), `backend` is still undefined and the row records `backend: 'unknown'` as before. When `requireCapability()` throws, `backend` is the resolved instance and the row records its actual name. Test: new "records capability-mismatch on generateStream with the resolved backend name" exercises a backend registered with `stream: false` and asserts `backend: 'no-stream'` (not 'unknown') + `error_code: 'capability_unsupported'`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fabef73 commit 3c24fe3

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

resources/models/Models.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,17 @@ export class Models implements ModelsContract {
8181
generateStream(input: GenerateInput, opts: GenerateOpts = {}): AsyncIterable<GenerateChunk> {
8282
const { accounting, signal } = resolveCallContext(opts.signal);
8383
const startedAt = performance.now();
84-
let backend: ModelBackend;
84+
let backend: ModelBackend | undefined;
8585
try {
8686
backend = resolveGenerative(opts.model);
8787
requireCapability(backend, 'stream');
8888
} catch (err) {
8989
// Record pre-call failure synchronously so callers that hold but never
9090
// iterate the returned iterable still produce a billing row, then rethrow.
91-
this.#recordFailure(undefined, 'generateStream', opts.model, accounting, opts, startedAt, err);
91+
// Pass `backend` (not `undefined`): when the registry hit but the capability
92+
// check failed, `backend` is the resolved instance and its name belongs in
93+
// the row. Only registry misses leave `backend` undefined → 'unknown'.
94+
this.#recordFailure(backend, 'generateStream', opts.model, accounting, opts, startedAt, err);
9295
throw err;
9396
}
9497
const backendOpts: BackendOpts<GenerateOpts> = { ...opts, signal, accounting };

unitTests/resources/models/Models.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,5 +275,22 @@ describe('Models facade', () => {
275275
assert.strictEqual(writer.records[0].error_code, 'backend_not_found');
276276
assert.strictEqual(writer.records[0].method, 'generateStream');
277277
});
278+
279+
it('records capability-mismatch on generateStream with the resolved backend name (not "unknown")', async () => {
280+
setGenerative('default', {
281+
name: 'no-stream',
282+
capabilities: () => ({ embed: false, generate: true, stream: false, tools: false, adapters: false }),
283+
});
284+
await assert.rejects(async () => {
285+
// eslint-disable-next-line no-unused-vars
286+
for await (const _ of models.generateStream('x')) {
287+
// will not iterate — requireCapability throws first
288+
}
289+
});
290+
assert.strictEqual(writer.records.length, 1);
291+
assert.strictEqual(writer.records[0].backend, 'no-stream');
292+
assert.strictEqual(writer.records[0].error_code, 'capability_unsupported');
293+
assert.strictEqual(writer.records[0].method, 'generateStream');
294+
});
278295
});
279296
});

0 commit comments

Comments
 (0)