fix(glmt): emit thinking signature as opaque string

generateThinkingSignature() returned an object
({type, hash, length, timestamp}) but Anthropic requires a thinking
block's `signature` to be a non-empty opaque STRING. The object made
the thinking block invalid, breaking the stream against reasoning
backends and surfacing as a false 502 "did not respond within 600s".

Return a deterministic base64 token instead (no Date.now()), and
narrow ThinkingSignature to a string alias so existing imports stay
intact. The signature is only ever assigned, never read as an object,
so consumers are unaffected. Update the existing unit test to the
corrected string contract.

Built [OnSteroids](https://onsteroids.ai)
This commit is contained in:
Sergey Galuza committed 2026-06-29 13:27:09 +02:00
1 parent 37d2bae001
commit cbcdb6ba62
3 files changed
+17 -20

No files matched your search

+6 -7
View File
@@ -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');
}
/**
+4 -7
View File
@@ -7,7 +7,7 @@ export interface ContentBlock {
type: string;
text?: string;
thinking?: string;
signature?: ThinkingSignature;
signature?: string;
id?: string;
name?: string;
input?: Record<string, unknown>;
@@ -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 {
+7 -6
View File
@@ -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));
});
});