Skip to content

Commit 25795a5

Browse files
committed
cleanup types
1 parent 7cdffd4 commit 25795a5

File tree

9 files changed

+235
-208
lines changed

9 files changed

+235
-208
lines changed

src/funcs/call-model.ts

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
import type { OpenRouterCore } from '../core.js';
2-
import type { AsyncCallModelInput } from '../lib/async-params.js';
2+
import type { CallModelInput } from '../lib/async-params.js';
33
import type { RequestOptions } from '../lib/sdks.js';
4-
import type { MaxToolRounds, Tool } from '../lib/tool-types.js';
5-
import type * as models from '../models/index.js';
64

75
import { ModelResult } from '../lib/model-result.js';
86
import { convertToolsToAPIFormat } from '../lib/tool-executor.js';
97

10-
/**
11-
* Input type for callModel function
12-
*/
13-
export type CallModelInput = Omit<models.OpenResponsesRequest, 'stream' | 'tools'> & {
14-
tools?: Tool[];
15-
maxToolRounds?: MaxToolRounds;
16-
};
17-
18-
// Re-export AsyncCallModelInput for convenience
19-
export type { AsyncCallModelInput } from '../lib/async-params.js';
8+
// Re-export CallModelInput for convenience
9+
export type { CallModelInput } from '../lib/async-params.js';
2010

2111
/**
2212
* Get a response with multiple consumption patterns
@@ -44,16 +34,17 @@ export type { AsyncCallModelInput } from '../lib/async-params.js';
4434
* **Async Function Support:**
4535
*
4636
* Any field in CallModelInput can be a function that computes the value dynamically
47-
* based on the conversation context. Functions are resolved before EVERY turn, allowing
48-
* parameters to adapt as the conversation progresses.
37+
* based on the conversation context. You can mix static values and functions in the
38+
* same request. Functions are resolved before EVERY turn, allowing parameters to
39+
* adapt as the conversation progresses.
4940
*
5041
* @example
5142
* ```typescript
52-
* // Increase temperature over turns
43+
* // Mix static and dynamic values
5344
* const result = callModel(client, {
54-
* model: 'gpt-4',
55-
* temperature: (ctx) => Math.min(ctx.numberOfTurns * 0.2, 1.0),
56-
* input: [{ type: 'text', text: 'Hello' }],
45+
* model: 'gpt-4', // static
46+
* temperature: (ctx) => Math.min(ctx.numberOfTurns * 0.2, 1.0), // dynamic
47+
* input: [{ type: 'text', text: 'Hello' }], // static
5748
* });
5849
* ```
5950
*
@@ -94,7 +85,7 @@ export type { AsyncCallModelInput } from '../lib/async-params.js';
9485
*/
9586
export function callModel(
9687
client: OpenRouterCore,
97-
request: CallModelInput | AsyncCallModelInput,
88+
request: CallModelInput,
9889
options?: RequestOptions,
9990
): ModelResult {
10091
const { tools, maxToolRounds, ...apiRequest } = request;
@@ -104,12 +95,14 @@ export function callModel(
10495

10596
// Build the request with converted tools
10697
// Note: async functions are resolved later in ModelResult.executeToolsIfNeeded()
107-
const finalRequest: models.OpenResponsesRequest | AsyncCallModelInput = {
98+
// The request can have async fields (functions) or sync fields, and the tools are converted to API format
99+
const finalRequest: Record<string, unknown> = {
108100
...apiRequest,
109-
...(apiTools !== undefined && {
110-
tools: apiTools,
111-
}),
112-
} as any;
101+
};
102+
103+
if (apiTools !== undefined) {
104+
finalRequest['tools'] = apiTools;
105+
}
113106

114107
return new ModelResult({
115108
client,

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
// Async params support
66
export type {
7-
AsyncCallModelInput,
7+
CallModelInput,
88
FieldOrAsyncFunction,
9-
ResolvedAsyncCallModelInput,
9+
ResolvedCallModelInput,
1010
} from './lib/async-params.js';
1111
export type { Fetcher, HTTPClientOptions } from './lib/http.js';
1212
// Tool types

src/lib/async-params.ts

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
1-
import type { CallModelInput } from '../funcs/call-model.js';
2-
import type { TurnContext } from './tool-types.js';
1+
import type * as models from '../models/index.js';
2+
import type { MaxToolRounds, Tool, TurnContext } from './tool-types.js';
33

44
/**
55
* A field can be either a value of type T or a function that computes T
66
*/
77
export type FieldOrAsyncFunction<T> = T | ((context: TurnContext) => T | Promise<T>);
88

99
/**
10-
* CallModelInput with async function support for API parameter fields
11-
* Excludes tools and maxToolRounds which should not be dynamic
10+
* Input type for callModel function
11+
* Each field can independently be a static value or a function that computes the value
1212
*/
13-
export type AsyncCallModelInput = {
14-
[K in keyof Omit<CallModelInput, 'tools' | 'maxToolRounds'>]: FieldOrAsyncFunction<
15-
CallModelInput[K]
16-
>;
13+
export type CallModelInput = {
14+
[K in keyof Omit<models.OpenResponsesRequest, 'stream' | 'tools'>]?:
15+
FieldOrAsyncFunction<models.OpenResponsesRequest[K]>;
1716
} & {
18-
tools?: CallModelInput['tools'];
19-
maxToolRounds?: CallModelInput['maxToolRounds'];
17+
tools?: Tool[];
18+
maxToolRounds?: MaxToolRounds;
2019
};
2120

2221
/**
23-
* Resolved AsyncCallModelInput (all functions evaluated to values)
24-
* This strips out the function types, leaving only the resolved value types
22+
* Resolved CallModelInput (all functions evaluated to values)
23+
* This is the type after all async functions have been resolved to their values
2524
*/
26-
export type ResolvedAsyncCallModelInput = Omit<CallModelInput, 'tools' | 'maxToolRounds'> & {
25+
export type ResolvedCallModelInput = Omit<models.OpenResponsesRequest, 'stream' | 'tools'> & {
2726
tools?: never;
2827
maxToolRounds?: never;
2928
};
@@ -49,32 +48,36 @@ export type ResolvedAsyncCallModelInput = Omit<CallModelInput, 'tools' | 'maxToo
4948
* ```
5049
*/
5150
export async function resolveAsyncFunctions(
52-
input: AsyncCallModelInput,
51+
input: CallModelInput,
5352
context: TurnContext,
54-
): Promise<ResolvedAsyncCallModelInput> {
55-
const resolved: Record<string, unknown> = {};
53+
): Promise<ResolvedCallModelInput> {
54+
// Build the resolved object by processing each field
55+
const resolvedEntries: Array<[string, unknown]> = [];
5656

5757
// Iterate over all keys in the input
5858
for (const [key, value] of Object.entries(input)) {
5959
if (typeof value === 'function') {
6060
try {
61-
// Execute the function with context
62-
resolved[key] = await Promise.resolve(value(context));
61+
// Execute the function with context and store the result
62+
const result = await Promise.resolve(value(context));
63+
resolvedEntries.push([key, result]);
6364
} catch (error) {
6465
// Wrap errors with context about which field failed
6566
throw new Error(
66-
`Failed to resolve async function for field "${key}": ${
67-
error instanceof Error ? error.message : String(error)
67+
`Failed to resolve async function for field "${key}": ${error instanceof Error ? error.message : String(error)
6868
}`,
6969
);
7070
}
7171
} else {
7272
// Not a function, use as-is
73-
resolved[key] = value;
73+
resolvedEntries.push([key, value]);
7474
}
7575
}
7676

77-
return resolved as ResolvedAsyncCallModelInput;
77+
// Build the final object from entries
78+
// Type safety is ensured by the input type - each key in CallModelInput
79+
// corresponds to the same key in ResolvedCallModelInput with resolved type
80+
return Object.fromEntries(resolvedEntries) as ResolvedCallModelInput;
7881
}
7982

8083
/**
@@ -83,7 +86,7 @@ export async function resolveAsyncFunctions(
8386
* @param input - Input to check
8487
* @returns True if any field is a function
8588
*/
86-
export function hasAsyncFunctions(input: any): boolean {
89+
export function hasAsyncFunctions(input: unknown): boolean {
8790
if (!input || typeof input !== 'object') {
8891
return false;
8992
}

src/lib/model-result.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { OpenRouterCore } from '../core.js';
22
import type * as models from '../models/index.js';
3-
import type { AsyncCallModelInput } from './async-params.js';
3+
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 {
@@ -83,7 +83,9 @@ function hasTypeProperty(item: unknown): item is {
8383
}
8484

8585
export interface GetResponseOptions {
86-
request: models.OpenResponsesRequest | AsyncCallModelInput;
86+
// Request can be a mix of sync and async fields
87+
// The actual type will be narrowed during async function resolution
88+
request: models.OpenResponsesRequest | CallModelInput | Record<string, unknown>;
8789
client: OpenRouterCore;
8890
options?: RequestOptions;
8991
tools?: Tool[];
@@ -164,7 +166,7 @@ export class ModelResult {
164166
// Resolve any async functions first
165167
if (hasAsyncFunctions(this.options.request)) {
166168
const resolved = await resolveAsyncFunctions(
167-
this.options.request as AsyncCallModelInput,
169+
this.options.request as CallModelInput,
168170
initialContext,
169171
);
170172
this.options.request = resolved as models.OpenResponsesRequest;
@@ -316,7 +318,7 @@ export class ModelResult {
316318
// Resolve async functions for this turn
317319
if (hasAsyncFunctions(this.options.request)) {
318320
const resolved = await resolveAsyncFunctions(
319-
this.options.request as AsyncCallModelInput,
321+
this.options.request as CallModelInput,
320322
turnContext,
321323
);
322324
this.options.request = resolved as models.OpenResponsesRequest;

src/lib/next-turn-params.ts

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import type * as models from '../models/index.js';
22
import type { NextTurnParamsContext, ParsedToolCall, Tool } from './tool-types.js';
33

4+
/**
5+
* Type guard to check if a value is a Record<string, unknown>
6+
*/
7+
function isRecord(value: unknown): value is Record<string, unknown> {
8+
return typeof value === 'object' && value !== null && !Array.isArray(value);
9+
}
10+
411
/**
512
* Build a NextTurnParamsContext from the current request
613
* Extracts relevant fields that can be modified by nextTurnParams functions
@@ -40,13 +47,10 @@ export async function executeNextTurnParamsFunctions(
4047
// Build initial context from current request
4148
const context = buildNextTurnParamsContext(currentRequest);
4249

43-
// Group tool calls by parameter they modify
44-
const paramFunctions = new Map<
45-
keyof NextTurnParamsContext,
46-
Array<{ params: Record<string, unknown>; fn: (params: Record<string, unknown>, context: NextTurnParamsContext) => unknown }>
47-
>();
48-
4950
// Collect all nextTurnParams functions from tools (in tools array order)
51+
const result: Partial<NextTurnParamsContext> = {};
52+
let workingContext = { ...context };
53+
5054
for (const tool of tools) {
5155
if (!tool.function.nextTurnParams) {
5256
continue;
@@ -57,41 +61,83 @@ export async function executeNextTurnParamsFunctions(
5761

5862
for (const call of callsForTool) {
5963
// For each parameter function in this tool's nextTurnParams
60-
for (const [paramKey, fn] of Object.entries(tool.function.nextTurnParams)) {
61-
if (!paramFunctions.has(paramKey as keyof NextTurnParamsContext)) {
62-
paramFunctions.set(paramKey as keyof NextTurnParamsContext, []);
63-
}
64-
paramFunctions.get(paramKey as keyof NextTurnParamsContext)!.push({
65-
params: call.arguments as Record<string, unknown>,
66-
fn: fn as (params: Record<string, unknown>, context: NextTurnParamsContext) => unknown,
67-
});
64+
// We need to process each key individually to maintain type safety
65+
const nextParams = tool.function.nextTurnParams;
66+
67+
// Validate that call.arguments is a record using type guard
68+
if (!isRecord(call.arguments)) {
69+
throw new Error(
70+
`Tool call arguments for ${tool.function.name} must be an object, got ${typeof call.arguments}`
71+
);
6872
}
73+
74+
// Process each parameter key with proper typing
75+
await processNextTurnParamsForCall(nextParams, call.arguments, workingContext, result);
6976
}
7077
}
7178

72-
// Compose and execute functions for each parameter
73-
const result: Partial<NextTurnParamsContext> = {};
74-
let workingContext = { ...context };
79+
return result;
80+
}
7581

76-
for (const [paramKey, functions] of paramFunctions.entries()) {
77-
// Compose all functions for this parameter
78-
let currentValue = workingContext[paramKey];
82+
/**
83+
* Process nextTurnParams for a single tool call with full type safety
84+
*/
85+
async function processNextTurnParamsForCall(
86+
nextParams: Record<string, unknown>,
87+
params: Record<string, unknown>,
88+
workingContext: NextTurnParamsContext,
89+
result: Partial<NextTurnParamsContext>
90+
): Promise<void> {
91+
// Type-safe processing for each known parameter key
92+
// We iterate through keys and use runtime checks instead of casts
93+
for (const paramKey of Object.keys(nextParams)) {
94+
const fn = nextParams[paramKey];
7995

80-
for (const { params, fn } of functions) {
81-
// Update context with current value
82-
workingContext = { ...workingContext, [paramKey]: currentValue };
96+
if (typeof fn !== 'function') {
97+
continue;
98+
}
8399

84-
// Execute function with composition
85-
// Type assertion needed because fn returns unknown but we know it returns the correct type
86-
currentValue = await Promise.resolve(fn(params, workingContext)) as typeof currentValue;
100+
// Validate that paramKey is actually a key of NextTurnParamsContext
101+
if (!isValidNextTurnParamKey(paramKey)) {
102+
// Skip invalid keys silently - they're not part of the API
103+
continue;
87104
}
88105

89-
// TypeScript can't infer that paramKey corresponds to the correct value type
90-
// so we use a type assertion here
91-
(result as any)[paramKey] = currentValue;
106+
// Execute the function and await the result
107+
const newValue = await Promise.resolve(fn(params, workingContext));
108+
109+
// Update the result using type-safe assignment
110+
setNextTurnParam(result, paramKey, newValue);
92111
}
112+
}
93113

94-
return result;
114+
/**
115+
* Type guard to check if a string is a valid NextTurnParamsContext key
116+
*/
117+
function isValidNextTurnParamKey(key: string): key is keyof NextTurnParamsContext {
118+
const validKeys: ReadonlySet<string> = new Set([
119+
'input',
120+
'model',
121+
'models',
122+
'temperature',
123+
'maxOutputTokens',
124+
'topP',
125+
'topK',
126+
'instructions',
127+
]);
128+
return validKeys.has(key);
129+
}
130+
131+
/**
132+
* Type-safe setter for NextTurnParamsContext
133+
* Ensures the value type matches the key type
134+
*/
135+
function setNextTurnParam<K extends keyof NextTurnParamsContext>(
136+
target: Partial<NextTurnParamsContext>,
137+
key: K,
138+
value: NextTurnParamsContext[K]
139+
): void {
140+
target[key] = value;
95141
}
96142

97143
/**

0 commit comments

Comments
 (0)