Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
99941fc
feat: add nextTurnParams feature for dynamic conversation steering
mattapperson Dec 18, 2025
4b78a19
feat: add async function support for CallModelInput parameters
mattapperson Dec 18, 2025
7cdffd4
fix: correct type for nextTurnParams function parameters
mattapperson Dec 18, 2025
25795a5
cleanup types
mattapperson Dec 18, 2025
457f93b
fix
mattapperson Dec 18, 2025
c8ef55a
add stepWhen
mattapperson Dec 18, 2025
6369ea3
cleanup
mattapperson Dec 18, 2025
af4f852
type cleanup and cruft removal
mattapperson Dec 18, 2025
f48d9f2
improve shape of turn context
mattapperson Dec 18, 2025
f7e2849
fixes and improvements
mattapperson Dec 18, 2025
bbab16a
fix: add stream termination and stopWhen condition support
mattapperson Dec 18, 2025
701fe67
fix: use Claude Sonnet 4.5 with toolChoice required for reliable tool…
mattapperson Dec 18, 2025
61ecacd
fix: differentiate manual vs auto-execution in tool tests
mattapperson Dec 18, 2025
8f553df
fix: tools were being stripped from API requests
mattapperson Dec 18, 2025
73de904
fix: increase timeout for tool execution test to 60s
mattapperson Dec 18, 2025
9a11734
test: skip flaky tool execution test in CI
mattapperson Dec 18, 2025
c3308b7
fix getNewMessagesStream
mattapperson Dec 18, 2025
4b288a9
need a longer timeout
mattapperson Dec 18, 2025
4e96888
unskip tests that now work
mattapperson Dec 18, 2025
8f0d240
add error expectation
mattapperson Dec 18, 2025
21df4ea
cleanup
mattapperson Dec 18, 2025
a23183f
longer timeout
mattapperson Dec 18, 2025
306b3b0
add env protection from log spam
mattapperson Dec 18, 2025
ad22de4
filename change
mattapperson Dec 18, 2025
7b03504
AGENTS -> CLAUDE
subtleGradient Dec 19, 2025
14b3340
cleanup clod
subtleGradient Dec 19, 2025
feb5273
cleanup
subtleGradient Dec 19, 2025
7a42ef6
zeditor settings
subtleGradient Dec 19, 2025
f530180
TOOLS -> TTools
subtleGradient Dec 19, 2025
a635ca1
nits--
subtleGradient Dec 19, 2025
5d00372
typecheck
subtleGradient Dec 19, 2025
1941871
unbreak bad edit
subtleGradient Dec 19, 2025
c5d243c
Add test scripts and fix whitespace
subtleGradient Dec 19, 2025
33a174c
use typeguard
subtleGradient Dec 19, 2025
880135c
deflake test
subtleGradient Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .zed/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"tab_size": 2,
"project_name": "@openrouter/sdk",
"formatter": {
"language_server": {
"name": "eslint"
}
},
"language_servers": ["!biome", "..."]
}
1 change: 1 addition & 0 deletions AGENTS.md
218 changes: 218 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

This is the OpenRouter TypeScript SDK - a type-safe toolkit for building AI applications with access to 300+ language models. The SDK is **generated using Speakeasy** from an OpenAPI specification, with custom hand-written features for tool orchestration, async parameter resolution, and streaming.

**IMPORTANT**: Most code in this repository is auto-generated by Speakeasy. Do not manually edit generated files - changes will be overwritten. See the "Code Generation" section below for how to make changes.

## Common Commands

### Building
```bash
pnpm run build
```
Compiles TypeScript to `esm/` directory using `tsc`.

### Linting
```bash
pnpm run lint
```
**Note**: This project uses **ESLint** (not Biome). Configuration is in `eslint.config.mjs`.

### Testing
```bash
# Run all tests
npx vitest

# Run specific test file
npx vitest tests/e2e/call-model.test.ts

# Run tests in watch mode
npx vitest --watch
```

Tests require an OpenRouter API key:
1. Copy `.env.example` to `.env`
2. Add your API key: `OPENROUTER_API_KEY=your_key_here`

Test organization:
- `tests/e2e/` - End-to-end integration tests
- `tests/unit/` - Unit tests
- `tests/funcs/` - Function-specific tests

### Publishing
```bash
pnpm run prepublishOnly
```
This runs the build automatically before publishing.

## Code Generation with Speakeasy

The SDK is generated from `.speakeasy/in.openapi.yaml` using [Speakeasy](https://www.speakeasy.com/docs).

### Generated vs Hand-Written Code

**Generated Files** (DO NOT EDIT - will be overwritten):
- `src/models/` - Type definitions from OpenAPI schemas
- `src/funcs/*Send.ts`, `src/funcs/*Get.ts`, etc. - Most API operation functions
- `src/sdk/` - SDK service classes
- `src/hooks/registration.ts` - Hook registration

**Hand-Written Files** (safe to edit):
- `src/lib/` - All library utilities and helpers
- `src/funcs/call-model.ts` - High-level model calling abstraction
- `src/index.ts` - Main exports
- `src/hooks/hooks.ts` and `src/hooks/types.ts` - Custom hooks

### Regenerating the SDK

To regenerate after updating the OpenAPI spec:
```bash
speakeasy run
```

This reads configuration from `.speakeasy/gen.yaml` and workflow from `.speakeasy/workflow.yaml`.

### Making Changes to Generated Code

1. **For type/schema changes**: Update `.speakeasy/in.openapi.yaml` and regenerate
2. **For overlays**: Edit files in `.speakeasy/overlays/` to apply transformations
3. **For generation config**: Edit `.speakeasy/gen.yaml`
4. **Always commit both** the OpenAPI spec changes AND the regenerated code

## Architecture

### Core Abstractions

**callModel** (`src/funcs/call-model.ts`)
- High-level function for making model requests with tools
- Returns a `ModelResult` wrapper with multiple consumption patterns
- Supports async parameter resolution and automatic tool execution
- Example consumption: `.getText()`, `.getTextStream()`, `.getToolStream()`, etc.

**ModelResult** (`src/lib/model-result.ts`)
- Wraps streaming responses with multiple consumption patterns
- Handles automatic tool execution and turn orchestration
- Uses `ReusableReadableStream` to enable multiple parallel consumers

**Tool System** (`src/lib/tool.ts`, `src/lib/tool-types.ts`, `src/lib/tool-executor.ts`)
- `tool()` helper creates type-safe tools with Zod schemas
- Three tool types:
- **Regular tools** (`execute: function`) - auto-executed, return final result
- **Generator tools** (`execute: async generator`) - stream preliminary results
- **Manual tools** (`execute: false`) - return tool calls without execution
- Tool orchestrator (`src/lib/tool-orchestrator.ts`) manages multi-turn conversations

**Async Parameter Resolution** (`src/lib/async-params.ts`)
- Any parameter in `CallModelInput` can be a function: `(ctx: TurnContext) => value`
- Functions resolved before each turn, allowing dynamic parameter adjustment
- Supports both sync and async functions
- Example: `model: (ctx) => ctx.numberOfTurns > 3 ? 'gpt-4' : 'gpt-3.5-turbo'`

**Next Turn Params** (`src/lib/next-turn-params.ts`)
- Tools can define `nextTurnParams` to modify request parameters after execution
- Functions receive tool input and can return parameter updates
- Applied after tool execution, before next API request
- Example: Increase temperature after seeing tool results

**Stop Conditions** (`src/lib/stop-conditions.ts`)
- Control when tool execution loops terminate
- Built-in helpers: `stepCountIs()`, `hasToolCall()`, `maxTokensUsed()`, `maxCost()`, `finishReasonIs()`
- Custom conditions receive full step history
- Default: `stepCountIs(5)` if not specified

## Message Format Compatibility

The SDK supports multiple message formats:

- **OpenRouter format** (native)
- **Claude format** via `fromClaudeMessages()` / `toClaudeMessage()` (`src/lib/anthropic-compat.ts`)
- **OpenAI Chat format** via `fromChatMessages()` / `toChatMessage()` (`src/lib/chat-compat.ts`)

These converters handle content types, tool calls, and format-specific features.

## Streaming Architecture

**ReusableReadableStream** (`src/lib/reusable-stream.ts`)

- Caches stream events to enable multiple independent consumers
- Critical for allowing parallel consumption patterns (text + tools + reasoning)
- Handles both SSE and standard ReadableStream

**Stream Transformers** (`src/lib/stream-transformers.ts`)

- Extract specific data from response streams
- `extractTextDeltas()`, `extractReasoningDeltas()`, `extractToolDeltas()`
- Build higher-level streams for different consumption patterns
- Handle both streaming and non-streaming responses uniformly

## Development Workflow

### When Adding New Features

1. **If it's an API change**: Update `.speakeasy/in.openapi.yaml` in the monorepo (see `/Users/mattapperson/Development/CLAUDE.md` for monorepo workflow)
2. **If it's SDK functionality**: Add to `src/lib/` or extend existing hand-written files
3. **Add tests** to appropriate directory (`tests/e2e/`, `tests/unit/`)
4. **Update examples** if user-facing (in `examples/`)

### When Fixing Bugs

1. **In generated code**: Fix the OpenAPI spec or Speakeasy generation config, then regenerate
2. **In hand-written code**: Fix directly in `src/lib/` or other hand-written files
3. **Add regression test** to prevent reoccurrence

### Running Examples

```bash
cd examples
# Set your API key in .env first
node --loader ts-node/esm call-model.example.ts
```

Examples demonstrate:
- `call-model.example.ts` - Basic usage
- `call-model-typed-tool-calling.example.ts` - Type-safe tools
- `anthropic-multimodal-tools.example.ts` - Multimodal inputs with tools
- `anthropic-reasoning.example.ts` - Extended thinking/reasoning
- `chat-reasoning.example.ts` - Reasoning with chat format
- `tools-example.ts` - Comprehensive tool usage

## TypeScript Configuration

- **Target**: ES2020, module: Node16
- **Strict mode**: Enabled with strictest settings from tsconfig/bases
- **Output**: `esm/` directory with declaration files
- **Module format**: ESM only (no CommonJS)

Key compiler options:
- `exactOptionalPropertyTypes: true` - Strict optional handling
- `noUncheckedIndexedAccess: true` - Array access safety
- `isolatedModules: true` - Required for module transforms

## Testing Strategy

Tests use Vitest with:
- 30s timeout for API calls
- Environment variables from `.env`
- Type checking enabled for test files

E2E tests (`tests/e2e/`) make real API calls and test:
- Basic chat completions
- Tool execution flows
- Streaming responses
- Multi-turn conversations
- Different message formats

## Package Structure

This is an ES Module (ESM) package with multiple exports:
- `@openrouter/sdk` - Main SDK
- `@openrouter/sdk/types` - Type definitions
- `@openrouter/sdk/models` - Model types
- `@openrouter/sdk/models/operations` - Operation types
- `@openrouter/sdk/models/errors` - Error types

The package uses conditional exports in `package.json` to map source files to build outputs.
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ async function main() {
model: "openai/gpt-4o-mini",
input: "What's the weather like in Paris?",
tools: [weatherTool] as const,
maxToolRounds: 0, // Don't auto-execute, just get the tool calls
stopWhen: ({ steps }) => steps.length >= 0, // Stop immediately - don't auto-execute, just get the tool calls
});

// Tool calls are now typed based on the tool definitions!
Expand All @@ -117,7 +117,7 @@ async function main() {
model: "openai/gpt-4o-mini",
input: "What's the weather in Tokyo?",
tools: [weatherTool] as const,
maxToolRounds: 0,
stopWhen: ({ steps }) => steps.length >= 0, // Stop immediately
});

// Stream tool calls with typed arguments
Expand Down
File renamed without changes.
20 changes: 8 additions & 12 deletions examples/tools-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,20 @@
* 1. Validated using Zod schemas
* 2. Executed when the model calls them
* 3. Results sent back to the model
* 4. Process repeats until no more tool calls (up to maxToolRounds)
* 4. Process repeats until stopWhen condition is met (default: stepCountIs(5))
*
* The API is simple: just call callModel() with tools, and await the result.
* Tools are executed transparently before getMessage() or getText() returns!
*
* maxToolRounds can be:
* - A number: Maximum number of tool execution rounds (default: 5)
* - A function: (context: TurnContext) => boolean
* - Return true to allow another turn
* - Return false to stop execution
* - Context includes: numberOfTurns, messageHistory, model/models
* stopWhen can be:
* - A single condition: stepCountIs(3), hasToolCall('finalize'), maxCost(0.50)
* - An array of conditions: [stepCountIs(10), maxCost(1.00)] (OR logic - stops if ANY is true)
* - A custom function: ({ steps }) => steps.length >= 5 || steps.some(s => s.finishReason === 'length')
*/

import * as dotenv from 'dotenv';
import { z } from 'zod/v4';
import { OpenRouter, ToolType } from '../src/index.js';
import { OpenRouter, ToolType, stepCountIs } from '../src/index.js';

// Type declaration for ShadowRealm (TC39 Stage 3 proposal)
// See: https://tc39.es/proposal-shadowrealm/
Expand Down Expand Up @@ -78,10 +76,8 @@ async function basicToolExample() {
tools: [
weatherTool,
],
// Example: limit to 3 turns using a function
maxToolRounds: (context) => {
return context.numberOfTurns < 3; // Allow up to 3 turns
},
// Example: limit to 3 steps
stopWhen: stepCountIs(3),
});

// Tools are automatically executed! Just get the final message
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,13 @@
"scripts": {
"lint": "eslint --cache --max-warnings=0 src",
"build": "tsc",
"prepublishOnly": "npm run build"
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build",
"test": "vitest --run",
"test:watch": "vitest"
},
"peerDependencies": {

},
"devDependencies": {
"@eslint/js": "^9.19.0",
Expand Down
Loading