Skip to content

Commit f2c3d0d

Browse files
mattappersonclaude
andcommitted
refactor: replace type casts with proper type guards
- Create claude-constants.ts with constants for Claude block types - Create claude-type-guards.ts with isClaudeStyleMessages type guard - Replace `as Record<string, unknown>` casts with proper type narrowing - Remove `as unknown as TTools` double assertion in callModel.ts - Consolidate duplicate normalizeInputToArray functions - Consolidate createTurnContext and createTurnContextWithMutations methods - Add isGeneratorConfig type guard in create-tool.ts - Fix non-null assertion in tool-orchestrator.ts with resultHasError guard - Extend normalizeInputToArray to handle undefined input - Add comprehensive tests for new type guards 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cdf68ba commit f2c3d0d

File tree

12 files changed

+1137
-213
lines changed

12 files changed

+1137
-213
lines changed

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export type {
3131
TypedToolCall,
3232
TypedToolCallUnion,
3333
TurnContext,
34+
TurnChange,
35+
NextTurnParams,
3436
ParsedToolCall,
3537
// Event type inference helpers
3638
InferToolEvent,
@@ -40,4 +42,9 @@ export type {
4042
ToolStreamEvent,
4143
ChatStreamEvent,
4244
EnhancedResponseStreamEvent,
45+
// Builder types
46+
BuildTurnContextOptions,
4347
} from "./lib/tool-types.js";
48+
49+
// Tool helpers
50+
export { normalizeInputToArray, buildTurnContext } from "./lib/tool-types.js";

src/lib/claude-constants.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Claude content block types used in message content arrays
3+
*/
4+
export const ClaudeContentBlockType = {
5+
Text: "text",
6+
Image: "image",
7+
ToolUse: "tool_use",
8+
ToolResult: "tool_result",
9+
} as const;
10+
11+
export type ClaudeContentBlockType =
12+
(typeof ClaudeContentBlockType)[keyof typeof ClaudeContentBlockType];
13+
14+
/**
15+
* Message roles that exist in OpenAI/Chat format but NOT in Claude format.
16+
* Used to distinguish between the two message formats.
17+
*/
18+
export const NonClaudeMessageRole = {
19+
System: "system",
20+
Developer: "developer",
21+
Tool: "tool",
22+
} as const;
23+
24+
export type NonClaudeMessageRole =
25+
(typeof NonClaudeMessageRole)[keyof typeof NonClaudeMessageRole];

src/lib/claude-type-guards.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type * as models from "../models/index.js";
2+
import {
3+
ClaudeContentBlockType,
4+
NonClaudeMessageRole,
5+
} from "./claude-constants.js";
6+
7+
/**
8+
* Type guard: checks if a value is a valid object (not null, not array)
9+
*/
10+
function isRecord(value: unknown): value is Record<string, unknown> {
11+
return value !== null && typeof value === "object" && !Array.isArray(value);
12+
}
13+
14+
/**
15+
* Type guard: checks if a role is an OpenAI/Chat-specific role (not Claude)
16+
*/
17+
function isNonClaudeRole(role: unknown): boolean {
18+
return (
19+
role === NonClaudeMessageRole.System ||
20+
role === NonClaudeMessageRole.Developer ||
21+
role === NonClaudeMessageRole.Tool
22+
);
23+
}
24+
25+
/**
26+
* Type guard: checks if a content block is a Claude tool_result block
27+
*/
28+
function isClaudeToolResultBlock(block: unknown): boolean {
29+
if (!isRecord(block)) return false;
30+
return block["type"] === ClaudeContentBlockType.ToolResult;
31+
}
32+
33+
/**
34+
* Type guard: checks if a content block is a Claude image block with source object
35+
* (Claude uses 'source' object; OpenAI uses 'image_url' structure)
36+
*/
37+
function isClaudeImageBlockWithSource(block: unknown): boolean {
38+
if (!isRecord(block)) return false;
39+
return (
40+
block["type"] === ClaudeContentBlockType.Image &&
41+
"source" in block &&
42+
isRecord(block["source"])
43+
);
44+
}
45+
46+
/**
47+
* Type guard: checks if a content block is a Claude tool_use block with id
48+
* (Claude uses 'tool_use' blocks; OpenAI uses 'tool_calls' array on message)
49+
*/
50+
function isClaudeToolUseBlockWithId(block: unknown): boolean {
51+
if (!isRecord(block)) return false;
52+
return (
53+
block["type"] === ClaudeContentBlockType.ToolUse &&
54+
"id" in block &&
55+
typeof block["id"] === "string"
56+
);
57+
}
58+
59+
/**
60+
* Checks if a message's content array contains Claude-specific blocks
61+
*/
62+
function hasClaudeSpecificBlocks(content: unknown[]): boolean {
63+
for (const block of content) {
64+
if (isClaudeToolResultBlock(block)) return true;
65+
if (isClaudeImageBlockWithSource(block)) return true;
66+
if (isClaudeToolUseBlockWithId(block)) return true;
67+
}
68+
return false;
69+
}
70+
71+
/**
72+
* Type guard: checks if input is Anthropic Claude-style messages
73+
*
74+
* Claude messages have only 'user' or 'assistant' roles (no 'system', 'tool', 'developer')
75+
* and may contain Claude-specific block types in content arrays.
76+
*
77+
* We check for Claude-ONLY features to distinguish from OpenAI format:
78+
* - 'tool_result' blocks (Claude-specific; OpenAI uses role: 'tool')
79+
* - 'image' blocks with 'source' object (Claude-specific structure)
80+
* - 'tool_use' blocks with 'id' (Claude-specific; OpenAI uses 'tool_calls' array on message)
81+
*/
82+
export function isClaudeStyleMessages(
83+
input: unknown
84+
): input is models.ClaudeMessageParam[] {
85+
if (!Array.isArray(input) || input.length === 0) {
86+
return false;
87+
}
88+
89+
for (const msg of input) {
90+
// Skip non-object entries
91+
if (!isRecord(msg)) continue;
92+
93+
// Must have role property
94+
if (!("role" in msg)) continue;
95+
96+
// Claude messages don't have top-level 'type' field
97+
if ("type" in msg) continue;
98+
99+
// OpenAI has 'system', 'developer', 'tool' roles that Claude doesn't have
100+
// If we see these roles, it's definitely NOT Claude format
101+
if (isNonClaudeRole(msg["role"])) {
102+
return false;
103+
}
104+
105+
// Check for Claude-specific content blocks
106+
const content = msg["content"];
107+
if (Array.isArray(content) && hasClaudeSpecificBlocks(content)) {
108+
return true;
109+
}
110+
}
111+
112+
// No Claude-specific features found
113+
// Default to NOT Claude (prefer OpenAI chat format as it's more common)
114+
return false;
115+
}

0 commit comments

Comments
 (0)