Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 97 additions & 0 deletions .agents/skills/changeset-versioning/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Changeset Versioning & Release Workflow

## Overview

This repo uses [changesets](https://github.com/changesets/changesets) for version management and changelog generation. Publishing is **release-gated** via `workflow_dispatch` — it does NOT auto-publish on push to main.

## Why Release-Gated?

`@openrouter/agent` depends on `@openrouter/sdk`. When Speakeasy regenerates SDK types, the agent must be updated to match. Both packages need **coordinated releases** — publishing one without the other can break consumers.

## Adding a Changeset

When you make a change that should be included in the next release:

```bash
pnpm changeset add
```

This will prompt you to:
1. Select the package (`@openrouter/agent`)
2. Choose a bump type (`patch`, `minor`, `major`)
3. Write a summary of the change

This creates a markdown file in `.changeset/` that describes the change. Commit this file with your PR.

### Bump Type Guidelines

- **patch** — Bug fixes, type fixes, defensive coding improvements
- **minor** — New features, new exports, new tool capabilities
- **major** — Breaking API changes (e.g., changing `callModel` signature, removing exports)

## Release Flow

### Step 1: Version (creates a Version Packages PR)

Go to **Actions → Release → Run workflow** and select:
- **Mode:** `version`
- **Dry run:** unchecked

This runs `changesets/action` which:
- Consumes all pending `.changeset/*.md` files
- Bumps `package.json` version
- Updates `CHANGELOG.md`
- Opens a "chore: version packages" PR

Review and merge that PR to main.

### Step 2: Publish (pushes to npm)

After the Version Packages PR is merged, go to **Actions → Release → Run workflow** and select:
- **Mode:** `publish`
- **Dry run:** unchecked (or check it first to verify)

This runs `pnpm publish --provenance --access public` to push the new version to npm.

### Dry Run

To verify what would happen without making changes:
- **Mode:** `publish`
- **Dry run:** checked

## Coordination with @openrouter/sdk

Before releasing `@openrouter/agent`:
1. Ensure `@openrouter/sdk` has been published with any required type changes
2. Update the SDK dependency in `package.json` if needed
3. Run `pnpm install` to update the lockfile
4. Verify `pnpm run typecheck` and `pnpm run test` pass
5. Then proceed with the release flow above

## Changelog

Changelogs are auto-generated by `@changesets/changelog-github` and include:
- PR links
- Contributor attribution
- Commit references

The changelog is written to `CHANGELOG.md` during the version step.

## Configuration

- `.changeset/config.json` — Changesets configuration
- `.github/workflows/publish.yaml` — Release workflow (workflow_dispatch)
- `.github/workflows/ci.yaml` — PR validation (lint, typecheck, test)

## Common Commands

```bash
pnpm changeset add # Add a new changeset
pnpm changeset status # View pending changesets
pnpm changeset version # Apply changesets locally (usually done by CI)
pnpm run build # Build (tsc)
pnpm run typecheck # Type check without emitting
pnpm run test # Run unit tests
pnpm run test:e2e # Run e2e tests (requires OPENROUTER_API_KEY)
pnpm lint # Lint with eslint
```
42 changes: 37 additions & 5 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
name: Release

on:
push:
branches: [main]
workflow_dispatch:
inputs:
mode:
description: >
Release mode:
- "version" — Creates a "Version Packages" PR that bumps versions and
updates CHANGELOG.md based on pending changesets. Run this first.
- "publish" — Publishes the package to npm with provenance. Only run
this AFTER the Version Packages PR has been merged to main.
NOTE: @openrouter/agent releases must be coordinated with @openrouter/sdk
because callModel changes are typically coupled with SDK type changes.
required: true
type: choice
options:
- version
- publish
default: version
dry-run:
description: >
Dry run: If enabled, simulates the release without making any changes.
Useful for verifying what would happen before committing to a release.
required: false
type: boolean
default: false

permissions:
contents: write # For creating releases and the Version Packages PR
Expand Down Expand Up @@ -30,13 +52,23 @@ jobs:

- run: pnpm run test

- name: Create Release PR or Publish
id: changesets
- name: Create Version Packages PR
if: inputs.mode == 'version'
uses: changesets/action@v1
with:
publish: pnpm publish --no-git-checks --provenance --access public
title: "chore: version packages"
commit: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Publish to npm
if: inputs.mode == 'publish' && !inputs.dry-run
run: pnpm publish --no-git-checks --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish to npm (dry run)
if: inputs.mode == 'publish' && inputs.dry-run
run: pnpm publish --no-git-checks --provenance --access public --dry-run
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
107 changes: 107 additions & 0 deletions src/lib/next-turn-params.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type * as models from '@openrouter/sdk/models';

import { describe, expect, it } from 'vitest';
import { applyNextTurnParamsToRequest } from './next-turn-params.js';

/**
* Creates a minimal ResponsesRequest for testing applyNextTurnParamsToRequest.
*/
function createBaseRequest(
overrides?: Partial<models.ResponsesRequest>,
): models.ResponsesRequest {
return {
model: 'openai/gpt-4',
input: [{ role: 'user', content: 'hello' }],
...overrides,
};
}

describe('applyNextTurnParamsToRequest', () => {
it('should pass through non-null values unchanged', () => {
const request = createBaseRequest();
const result = applyNextTurnParamsToRequest(request, {
temperature: 0.5,
maxOutputTokens: 1000,
instructions: 'Be helpful',
});

expect(result.temperature).toBe(0.5);
expect(result.maxOutputTokens).toBe(1000);
expect(result.instructions).toBe('Be helpful');
});

it('should convert null values to undefined', () => {
const request = createBaseRequest({ temperature: 0.7 });
const result = applyNextTurnParamsToRequest(request, {
temperature: null,
maxOutputTokens: null,
instructions: null,
});

// null values become undefined, so the spread doesn't override
// with null — it overrides with undefined instead
expect(result.temperature).toBeUndefined();
expect(result.maxOutputTokens).toBeUndefined();
expect(result.instructions).toBeUndefined();
});

it('should preserve original request fields not in computedParams', () => {
const request = createBaseRequest({
model: 'anthropic/claude-3',
temperature: 0.9,
});
const result = applyNextTurnParamsToRequest(request, {
maxOutputTokens: 500,
});

expect(result.model).toBe('anthropic/claude-3');
expect(result.temperature).toBe(0.9);
expect(result.maxOutputTokens).toBe(500);
});

it('should handle empty computedParams without changing the request', () => {
const request = createBaseRequest({ temperature: 0.5 });
const result = applyNextTurnParamsToRequest(request, {});

expect(result.temperature).toBe(0.5);
expect(result.model).toBe('openai/gpt-4');
});

it('should handle mixed null and non-null values', () => {
const request = createBaseRequest({
temperature: 0.7,
topP: 0.9,
});
const result = applyNextTurnParamsToRequest(request, {
temperature: null,
topP: 0.5,
maxOutputTokens: null,
instructions: 'Updated instructions',
});

expect(result.temperature).toBeUndefined();
expect(result.topP).toBe(0.5);
expect(result.maxOutputTokens).toBeUndefined();
expect(result.instructions).toBe('Updated instructions');
});

it('should preserve zero as a valid non-null value', () => {
const request = createBaseRequest();
const result = applyNextTurnParamsToRequest(request, {
temperature: 0,
maxOutputTokens: 0,
});

expect(result.temperature).toBe(0);
expect(result.maxOutputTokens).toBe(0);
});

it('should preserve empty string as a valid non-null value', () => {
const request = createBaseRequest();
const result = applyNextTurnParamsToRequest(request, {
instructions: '' as string | null,
});

expect(result.instructions).toBe('');
});
});
8 changes: 7 additions & 1 deletion src/lib/next-turn-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,14 @@ export function applyNextTurnParamsToRequest(
request: models.ResponsesRequest,
computedParams: Partial<NextTurnParamsContext>,
): models.ResponsesRequest {
// Strip null values to undefined so they're compatible with ResponsesRequest
// fields that may be typed as `number | undefined` (without null)
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(computedParams)) {
sanitized[key] = value === null ? undefined : value;
}
return {
...request,
...computedParams,
...sanitized,
};
}
Loading