diff --git a/src/glmt/pipeline/response-builder.ts b/src/glmt/pipeline/response-builder.ts index 50862703..6426e288 100644 --- a/src/glmt/pipeline/response-builder.ts +++ b/src/glmt/pipeline/response-builder.ts @@ -177,17 +177,16 @@ export class ResponseBuilder { /** * Generate thinking signature for Claude Code UI + * + * Anthropic requires a thinking block's `signature` to be a non-empty + * opaque STRING. Emit a deterministic base64 token (no Date.now()) so the + * thinking block stays valid against reasoning backends; returning an object + * here produces an invalid thinking block and breaks the stream. */ generateThinkingSignature(thinking: string): ThinkingSignature { - // Generate signature hash const hash = crypto.createHash('sha256').update(thinking).digest('hex').substring(0, 16); - return { - type: 'thinking_signature', - hash: hash, - length: thinking.length, - timestamp: Date.now(), - }; + return Buffer.from(`ccs-thinking:${hash}:${thinking.length}`).toString('base64'); } /** diff --git a/src/glmt/pipeline/types.ts b/src/glmt/pipeline/types.ts index 27f68749..e18629ca 100644 --- a/src/glmt/pipeline/types.ts +++ b/src/glmt/pipeline/types.ts @@ -7,7 +7,7 @@ export interface ContentBlock { type: string; text?: string; thinking?: string; - signature?: ThinkingSignature; + signature?: string; id?: string; name?: string; input?: Record; @@ -95,12 +95,9 @@ export interface TransformResult { error?: string; } -export interface ThinkingSignature { - type: string; - hash: string; - length: number; - timestamp: number; -} +// Anthropic contract: a thinking block's signature is a non-empty opaque +// string. Kept as a named alias so existing imports stay intact. +export type ThinkingSignature = string; // OpenAI response types export interface OpenAIChoice { diff --git a/tests/unit/glmt/glmt-transformer.test.js b/tests/unit/glmt/glmt-transformer.test.js index 05950ba7..930fbb7e 100644 --- a/tests/unit/glmt/glmt-transformer.test.js +++ b/tests/unit/glmt/glmt-transformer.test.js @@ -161,16 +161,17 @@ describe('GlmtTransformer', () => { }); describe('Thinking signature', () => { - it('generates thinking signature', () => { + it('generates thinking signature as a non-empty opaque string', () => { const transformer = new GlmtTransformer(); const thinking = 'This is my reasoning process'; const signature = transformer.generateThinkingSignature(thinking); - assert.strictEqual(signature.type, 'thinking_signature'); - assert.ok(signature.hash); - assert.strictEqual(signature.hash.length, 16); - assert.strictEqual(signature.length, thinking.length); - assert.ok(signature.timestamp); + // Anthropic contract: a thinking block's signature is a non-empty + // opaque string, not an object. + assert.strictEqual(typeof signature, 'string'); + assert.ok(signature.length > 0); + // Deterministic: same reasoning yields the same signature (no Date.now()). + assert.strictEqual(signature, transformer.generateThinkingSignature(thinking)); }); });