Skip to content

Commit 21df4ea

Browse files
committed
cleanup
1 parent 8f0d240 commit 21df4ea

File tree

10 files changed

+135
-166
lines changed

10 files changed

+135
-166
lines changed

src/funcs/call-model.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { CallModelInput } from '../lib/async-params.js';
33
import type { RequestOptions } from '../lib/sdks.js';
44
import type { Tool } from '../lib/tool-types.js';
55

6-
import { ModelResult } from '../lib/model-result.js';
6+
import { ModelResult, type GetResponseOptions } from '../lib/model-result.js';
77
import { convertToolsToAPIFormat } from '../lib/tool-executor.js';
88

99
// Re-export CallModelInput for convenience
@@ -119,11 +119,11 @@ export type { CallModelInput } from '../lib/async-params.js';
119119
*
120120
* Default: `stepCountIs(5)` if not specified
121121
*/
122-
export function callModel<TOOLS extends readonly Tool[] = readonly Tool[]>(
122+
export function callModel<TOOLS extends readonly Tool[]>(
123123
client: OpenRouterCore,
124124
request: CallModelInput<TOOLS>,
125125
options?: RequestOptions,
126-
): ModelResult {
126+
): ModelResult<TOOLS> {
127127
const { tools, stopWhen, ...apiRequest } = request;
128128

129129
// Convert tools to API format - no cast needed now that convertToolsToAPIFormat accepts readonly
@@ -140,15 +140,14 @@ export function callModel<TOOLS extends readonly Tool[] = readonly Tool[]>(
140140
finalRequest['tools'] = apiTools;
141141
}
142142

143-
return new ModelResult({
143+
return new ModelResult<TOOLS>({
144144
client,
145145
request: finalRequest,
146146
options: options ?? {},
147-
// Cast to Tool[] because ModelResult expects mutable array internally
148-
// The readonly constraint is maintained at the callModel interface level
149-
tools: (tools ?? []) as Tool[],
147+
// Preserve the exact TOOLS type instead of widening to Tool[]
148+
tools: tools as TOOLS | undefined,
150149
...(stopWhen !== undefined && {
151150
stopWhen,
152151
}),
153-
});
152+
} as GetResponseOptions<TOOLS>);
154153
}

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,21 @@ export type { Fetcher, HTTPClientOptions } from './lib/http.js';
1212
// Tool types
1313
export type {
1414
ChatStreamEvent,
15-
EnhancedResponseStreamEvent,
15+
ResponseStreamEvent as EnhancedResponseStreamEvent,
1616
InferToolEvent,
1717
InferToolEventsUnion,
1818
InferToolInput,
1919
InferToolOutput,
2020
ManualTool,
2121
NextTurnParamsContext,
2222
NextTurnParamsFunctions,
23+
ParsedToolCall,
2324
StepResult,
2425
StopCondition,
2526
StopWhen,
2627
Tool,
28+
ToolExecutionResult,
29+
ToolExecutionResultUnion,
2730
ToolPreliminaryResultEvent,
2831
ToolStreamEvent,
2932
ToolWithExecute,

src/lib/model-result.ts

Lines changed: 28 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import type { CallModelInput } from './async-params.js';
44
import type { EventStream } from './event-streams.js';
55
import type { RequestOptions } from './sdks.js';
66
import type {
7-
ChatStreamEvent,
8-
EnhancedResponseStreamEvent,
7+
ResponseStreamEvent,
8+
InferToolEventsUnion,
99
ParsedToolCall,
1010
StopWhen,
1111
Tool,
@@ -52,24 +52,6 @@ function isEventStream(value: unknown): value is EventStream<models.OpenResponse
5252
);
5353
}
5454

55-
/**
56-
* Type guard for response.output_text.delta events
57-
*/
58-
function isOutputTextDeltaEvent(
59-
event: models.OpenResponsesStreamEvent,
60-
): event is models.OpenResponsesStreamEventResponseOutputTextDelta {
61-
return 'type' in event && event.type === 'response.output_text.delta';
62-
}
63-
64-
/**
65-
* Type guard for response.completed events
66-
*/
67-
function isResponseCompletedEvent(
68-
event: models.OpenResponsesStreamEvent,
69-
): event is models.OpenResponsesStreamEventResponseCompleted {
70-
return 'type' in event && event.type === 'response.completed';
71-
}
72-
7355
/**
7456
* Type guard for output items with a type property
7557
*/
@@ -88,13 +70,13 @@ function hasTypeProperty(item: unknown): item is {
8870
);
8971
}
9072

91-
export interface GetResponseOptions {
73+
export interface GetResponseOptions<TOOLS extends readonly Tool[]> {
9274
// Request can have async functions that will be resolved before sending to API
93-
request: CallModelInput;
75+
request: CallModelInput<TOOLS>;
9476
client: OpenRouterCore;
9577
options?: RequestOptions;
96-
tools?: Tool[];
97-
stopWhen?: StopWhen;
78+
tools?: TOOLS;
79+
stopWhen?: StopWhen<TOOLS>;
9880
}
9981

10082
/**
@@ -113,26 +95,28 @@ export interface GetResponseOptions {
11395
*
11496
* All consumption patterns can be used concurrently thanks to the underlying
11597
* ReusableReadableStream implementation.
98+
*
99+
* @template TOOLS - The tools array type to enable typed tool calls and results
116100
*/
117-
export class ModelResult {
101+
export class ModelResult<TOOLS extends readonly Tool[]> {
118102
private reusableStream: ReusableReadableStream<models.OpenResponsesStreamEvent> | null = null;
119103
private streamPromise: Promise<EventStream<models.OpenResponsesStreamEvent>> | null = null;
120104
private textPromise: Promise<string> | null = null;
121-
private options: GetResponseOptions;
105+
private options: GetResponseOptions<TOOLS>;
122106
private initPromise: Promise<void> | null = null;
123107
private toolExecutionPromise: Promise<void> | null = null;
124108
private finalResponse: models.OpenResponsesNonStreamingResponse | null = null;
125109
private preliminaryResults: Map<string, unknown[]> = new Map();
126110
private allToolExecutionRounds: Array<{
127111
round: number;
128-
toolCalls: ParsedToolCall[];
112+
toolCalls: ParsedToolCall<Tool>[];
129113
response: models.OpenResponsesNonStreamingResponse;
130114
toolResults: Array<models.OpenResponsesFunctionCallOutput>;
131115
}> = [];
132116
// Track resolved request after async function resolution
133117
private resolvedRequest: models.OpenResponsesRequest | null = null;
134118

135-
constructor(options: GetResponseOptions) {
119+
constructor(options: GetResponseOptions<TOOLS>) {
136120
this.options = options;
137121
}
138122

@@ -498,8 +482,8 @@ export class ModelResult {
498482
* Multiple consumers can iterate over this stream concurrently.
499483
* Includes preliminary tool result events after tool execution.
500484
*/
501-
getFullResponsesStream(): AsyncIterableIterator<EnhancedResponseStreamEvent> {
502-
return async function* (this: ModelResult) {
485+
getFullResponsesStream(): AsyncIterableIterator<ResponseStreamEvent<InferToolEventsUnion<TOOLS>>> {
486+
return async function* (this: ModelResult<TOOLS>) {
503487
await this.initStream();
504488
if (!this.reusableStream) {
505489
throw new Error('Stream not initialized');
@@ -521,7 +505,7 @@ export class ModelResult {
521505
yield {
522506
type: 'tool.preliminary_result' as const,
523507
toolCallId,
524-
result,
508+
result: result as InferToolEventsUnion<TOOLS>,
525509
timestamp: Date.now(),
526510
};
527511
}
@@ -534,7 +518,7 @@ export class ModelResult {
534518
* This filters the full event stream to only yield text content.
535519
*/
536520
getTextStream(): AsyncIterableIterator<string> {
537-
return async function* (this: ModelResult) {
521+
return async function* (this: ModelResult<TOOLS>) {
538522
await this.initStream();
539523
if (!this.reusableStream) {
540524
throw new Error('Stream not initialized');
@@ -553,7 +537,7 @@ export class ModelResult {
553537
getNewMessagesStream(): AsyncIterableIterator<
554538
models.ResponsesOutputMessage | models.OpenResponsesFunctionCallOutput
555539
> {
556-
return async function* (this: ModelResult) {
540+
return async function* (this: ModelResult<TOOLS>) {
557541
await this.initStream();
558542
if (!this.reusableStream) {
559543
throw new Error('Stream not initialized');
@@ -576,7 +560,7 @@ export class ModelResult {
576560
if (this.finalResponse && this.allToolExecutionRounds.length > 0) {
577561
// Check if the final response contains a message
578562
const hasMessage = this.finalResponse.output.some(
579-
(item) => hasTypeProperty(item) && item.type === 'message',
563+
(item: unknown) => hasTypeProperty(item) && item.type === 'message',
580564
);
581565
if (hasMessage) {
582566
yield extractResponsesMessageFromResponse(this.finalResponse);
@@ -585,12 +569,13 @@ export class ModelResult {
585569
}.call(this);
586570
}
587571

572+
588573
/**
589574
* Stream only reasoning deltas as they arrive.
590575
* This filters the full event stream to only yield reasoning content.
591576
*/
592577
getReasoningStream(): AsyncIterableIterator<string> {
593-
return async function* (this: ModelResult) {
578+
return async function* (this: ModelResult<TOOLS>) {
594579
await this.initStream();
595580
if (!this.reusableStream) {
596581
throw new Error('Stream not initialized');
@@ -606,8 +591,8 @@ export class ModelResult {
606591
* - Tool call argument deltas as { type: "delta", content: string }
607592
* - Preliminary results as { type: "preliminary_result", toolCallId, result }
608593
*/
609-
getToolStream(): AsyncIterableIterator<ToolStreamEvent> {
610-
return async function* (this: ModelResult) {
594+
getToolStream(): AsyncIterableIterator<ToolStreamEvent<InferToolEventsUnion<TOOLS>>> {
595+
return async function* (this: ModelResult<TOOLS>) {
611596
await this.initStream();
612597
if (!this.reusableStream) {
613598
throw new Error('Stream not initialized');
@@ -630,67 +615,7 @@ export class ModelResult {
630615
yield {
631616
type: 'preliminary_result' as const,
632617
toolCallId,
633-
result,
634-
};
635-
}
636-
}
637-
}.call(this);
638-
}
639-
640-
/**
641-
* Stream events in chat format (compatibility layer).
642-
* Note: This transforms responses API events into a chat-like format.
643-
* Includes preliminary tool result events after tool execution.
644-
*
645-
* @remarks
646-
* This is a compatibility method that attempts to transform the responses API
647-
* stream into a format similar to the chat API. Due to differences in the APIs,
648-
* this may not be a perfect mapping.
649-
*/
650-
getFullChatStream(): AsyncIterableIterator<ChatStreamEvent> {
651-
return async function* (this: ModelResult) {
652-
await this.initStream();
653-
if (!this.reusableStream) {
654-
throw new Error('Stream not initialized');
655-
}
656-
657-
const consumer = this.reusableStream.createConsumer();
658-
659-
for await (const event of consumer) {
660-
if (!('type' in event)) {
661-
continue;
662-
}
663-
664-
// Transform responses events to chat-like format using type guards
665-
if (isOutputTextDeltaEvent(event)) {
666-
yield {
667-
type: 'content.delta' as const,
668-
delta: event.delta,
669-
};
670-
} else if (isResponseCompletedEvent(event)) {
671-
yield {
672-
type: 'message.complete' as const,
673-
response: event.response,
674-
};
675-
} else {
676-
// Pass through other events
677-
yield {
678-
type: event.type,
679-
event,
680-
};
681-
}
682-
}
683-
684-
// After stream completes, check if tools were executed and emit preliminary results
685-
await this.executeToolsIfNeeded();
686-
687-
// Emit all preliminary results
688-
for (const [toolCallId, results] of this.preliminaryResults) {
689-
for (const result of results) {
690-
yield {
691-
type: 'tool.preliminary_result' as const,
692-
toolCallId,
693-
result,
618+
result: result as InferToolEventsUnion<TOOLS>,
694619
};
695620
}
696621
}
@@ -703,28 +628,28 @@ export class ModelResult {
703628
* and this will return the tool calls from the initial response.
704629
* Returns structured tool calls with parsed arguments.
705630
*/
706-
async getToolCalls(): Promise<ParsedToolCall[]> {
631+
async getToolCalls(): Promise<ParsedToolCall<TOOLS[number]>[]> {
707632
await this.initStream();
708633
if (!this.reusableStream) {
709634
throw new Error('Stream not initialized');
710635
}
711636

712637
const completedResponse = await consumeStreamForCompletion(this.reusableStream);
713-
return extractToolCallsFromResponse(completedResponse);
638+
return extractToolCallsFromResponse(completedResponse) as ParsedToolCall<TOOLS[number]>[];
714639
}
715640

716641
/**
717642
* Stream structured tool call objects as they're completed.
718643
* Each iteration yields a complete tool call with parsed arguments.
719644
*/
720-
getToolCallsStream(): AsyncIterableIterator<ParsedToolCall> {
721-
return async function* (this: ModelResult) {
645+
getToolCallsStream(): AsyncIterableIterator<ParsedToolCall<TOOLS[number]>> {
646+
return async function* (this: ModelResult<TOOLS>) {
722647
await this.initStream();
723648
if (!this.reusableStream) {
724649
throw new Error('Stream not initialized');
725650
}
726651

727-
yield* buildToolCallStream(this.reusableStream);
652+
yield* buildToolCallStream(this.reusableStream) as AsyncIterableIterator<ParsedToolCall<TOOLS[number]>>;
728653
}.call(this);
729654
}
730655

src/lib/next-turn-params.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export function buildNextTurnParamsContext(
4040
* @returns Object with computed parameter values
4141
*/
4242
export async function executeNextTurnParamsFunctions(
43-
toolCalls: ParsedToolCall[],
44-
tools: Tool[],
43+
toolCalls: ParsedToolCall<Tool>[],
44+
tools: readonly Tool[],
4545
currentRequest: models.OpenResponsesRequest
4646
): Promise<Partial<NextTurnParamsContext>> {
4747
// Build initial context from current request

0 commit comments

Comments
 (0)