diff --git a/bun.lock b/bun.lock index 9184da8c9..2ba689570 100644 --- a/bun.lock +++ b/bun.lock @@ -30,7 +30,7 @@ }, }, "packages/sdk": { - "name": "models.dev", + "name": "@opencode-ai/models", "version": "0.0.0", "devDependencies": { "@models.dev/core": "workspace:*", diff --git a/packages/sdk/LICENSE b/packages/sdk/LICENSE new file mode 100644 index 000000000..9ef000844 --- /dev/null +++ b/packages/sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 models.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/sdk/README.md b/packages/sdk/README.md index e941b4a18..ff168c0e7 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1,9 +1,9 @@ -# models.dev +# @opencode-ai/models Official typed client for the [models.dev](https://models.dev) API — an open-source database of AI model capabilities, pricing, and limits. ```sh -npm install models.dev +npm install @opencode-ai/models ``` - **Zero dependencies.** The root client is a small `fetch` wrapper; works on Node ≥ 18, Bun, Deno, browsers, and edge runtimes. @@ -13,7 +13,7 @@ npm install models.dev ## Usage ```ts -import { Models } from "models.dev" +import { Models } from "@opencode-ai/models" const client = Models.make() @@ -58,7 +58,7 @@ Errors are a single `ModelsDevError` with `reason: "Transport" | "UnexpectedStat A full copy of the database ships inside the package as a separate, tree-shakable entrypoint — nothing from it is loaded or bundled unless you import it: ```ts -import snapshot, { providers, models, generatedAt } from "models.dev/snapshot" +import snapshot, { providers, models, generatedAt } from "@opencode-ai/models/snapshot" providers["anthropic"]?.models["claude-opus-4-6"]?.limit.context ``` @@ -66,17 +66,17 @@ providers["anthropic"]?.models["claude-opus-4-6"]?.limit.context Use it for no-network runtimes, tests, cold-start-sensitive paths, or as an explicit fallback: ```ts -const providers = await client.providers().catch(async () => (await import("models.dev/snapshot")).providers) +const providers = await client.providers().catch(async () => (await import("@opencode-ai/models/snapshot")).providers) ``` Freshness: the published snapshot is at most ~24h behind the live API (data releases are automated). The client is the freshness path; the snapshot is the availability path. ## Effect -An Effect-native client lives at `models.dev/effect` (requires the optional peer dependency `effect`): +An Effect-native client lives at `@opencode-ai/models/effect` (requires the optional peer dependency `effect`): ```ts -import { Models } from "models.dev/effect" +import { Models } from "@opencode-ai/models/effect" import { FetchHttpClient } from "effect/unstable/http" import { Effect } from "effect" diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2cbd97bfb..19b30e05a 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "name": "models.dev", + "name": "@opencode-ai/models", "version": "0.0.0", "description": "Official typed client for the models.dev API \u2014 an open database of AI model capabilities, pricing, and limits", "type": "module", @@ -26,9 +26,18 @@ "node": ">=18" }, "exports": { - ".": "./src/index.ts", - "./effect": "./src/effect.ts", - "./snapshot": "./src/snapshot.js" + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./effect": { + "types": "./dist/effect.d.ts", + "default": "./dist/effect.js" + }, + "./snapshot": { + "types": "./dist/snapshot.d.ts", + "default": "./dist/snapshot.js" + } }, "files": [ "dist" @@ -36,6 +45,7 @@ "scripts": { "generate": "bun script/generate.ts", "build": "bun script/build.ts", + "prepack": "bun run build", "typecheck": "tsc --noEmit", "test": "bun run generate && bun run typecheck && bun test" }, diff --git a/packages/sdk/script/publish.ts b/packages/sdk/script/publish.ts index d086f1f81..8b770d0dd 100644 --- a/packages/sdk/script/publish.ts +++ b/packages/sdk/script/publish.ts @@ -1,12 +1,11 @@ #!/usr/bin/env bun -// Publishes models.dev to npm, opencode-style: -// - the version is never stored in git: it is `npm view models.dev version` +// Publishes @opencode-ai/models to npm, opencode-style: +// - the version is never stored in git: it is read from npm // plus a semver bump computed here (patch by default); // - `--if-changed` (scheduled data releases) skips publishing when the // freshly generated snapshot payload is byte-identical to the one inside // the currently published tarball; -// - package.json exports are rewritten src -> dist for the tarball and -// restored afterwards. +// - package.json and src/version.ts are restored after publishing. // // Auth: npm Trusted Publishing (OIDC) in CI — no token needed once the // package is linked to this repo+workflow on npmjs.com. `--provenance` is @@ -17,9 +16,9 @@ import { appendFile, mkdtemp, rm } from "node:fs/promises" import { tmpdir } from "node:os" import { $ } from "bun" import { loadCatalog, snapshotPayload } from "./generate.ts" -import { build } from "./build.ts" const pkg = path.join(import.meta.dirname, "..") +const packageName = "@opencode-ai/models" const packageJsonPath = path.join(pkg, "package.json") const versionTsPath = path.join(pkg, "src", "version.ts") @@ -32,11 +31,7 @@ if (!["patch", "minor", "major"].includes(bumpArg)) { } async function currentVersion(): Promise { - try { - return (await $`npm view models.dev version`.text()).trim() - } catch { - return "0.0.0" - } + return (await $`npm view ${packageName} version`.text()).trim() } function bump(version: string, kind: string): string { @@ -50,7 +45,7 @@ function bump(version: string, kind: string): string { async function publishedSnapshotLine(): Promise { const directory = await mkdtemp(path.join(tmpdir(), "models-dev-publish-")) try { - const tarball = (await $`npm pack models.dev@latest --pack-destination ${directory}`.cwd(directory).text()) + const tarball = (await $`npm pack ${packageName}@latest --pack-destination ${directory}`.cwd(directory).text()) .trim() .split("\n") .at(-1)! @@ -59,8 +54,6 @@ async function publishedSnapshotLine(): Promise { if (!(await file.exists())) return undefined const text = await file.text() return text.split("\n").find((line) => line.startsWith("const data = ")) - } catch { - return undefined } finally { await rm(directory, { recursive: true, force: true }) } @@ -80,27 +73,16 @@ if (ifChanged) { const current = await currentVersion() const next = bump(current, bumpArg) -const alreadyPublished = await $`npm view models.dev@${next} version`.quiet().nothrow().text() -if (alreadyPublished.trim() === next) { - console.log(`models.dev@${next} already published; skipping`) - process.exit(0) -} +console.log(`Publishing ${packageName}@${next} (${bumpArg} bump from ${current})`) -console.log(`Publishing models.dev@${next} (${bumpArg} bump from ${current})`) - -const packageJson = await Bun.file(packageJsonPath).json() +const packageJsonText = await Bun.file(packageJsonPath).text() +const packageJson = JSON.parse(packageJsonText) const versionTs = await Bun.file(versionTsPath).text() try { await Bun.write(versionTsPath, versionTs.replace('"0.0.0"', JSON.stringify(next))) - await build() packageJson.version = next - packageJson.exports = { - ".": { types: "./dist/index.d.ts", default: "./dist/index.js" }, - "./effect": { types: "./dist/effect.d.ts", default: "./dist/effect.js" }, - "./snapshot": { types: "./dist/snapshot.d.ts", default: "./dist/snapshot.js" }, - } await Bun.write(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n") const provenance = process.env["GITHUB_ACTIONS"] === "true" ? ["--provenance"] : [] @@ -108,7 +90,7 @@ try { const output = process.env["GITHUB_OUTPUT"] if (output !== undefined) await appendFile(output, `version=${next}\n`) - console.log(`Published models.dev@${next}`) + console.log(`Published ${packageName}@${next}`) } finally { - await $`git checkout -- ${packageJsonPath} ${versionTsPath}`.nothrow() + await Promise.all([Bun.write(packageJsonPath, packageJsonText), Bun.write(versionTsPath, versionTs)]) } diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index e0fab8e82..f63aa5c36 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -1,6 +1,5 @@ import { ModelsDevError } from "./error.js" import type { Catalog, ModelMetadataMap, ProviderMap } from "./types.js" -import { VERSION } from "./version.js" /** Accepted anywhere headers can be passed. Same shapes as the standard `HeadersInit`. */ export type HeadersInput = Headers | Record | Array<[string, string]> @@ -28,7 +27,7 @@ export interface RequestOptions { * Creates a stateless models.dev client. Every method performs exactly one * `GET` and nothing is ever cached — callers who want caching should wrap * calls with their own policy. For a no-network alternative, see the - * `models.dev/snapshot` entrypoint. + * `@opencode-ai/models/snapshot` entrypoint. */ export function make(options: ClientOptions = {}) { const baseUrl = options.baseUrl ?? "https://models.dev" @@ -36,7 +35,7 @@ export function make(options: ClientOptions = {}) { const request = async (path: string, requestOptions?: RequestOptions): Promise => { const fetch = options.fetch ?? globalThis.fetch - const headers = new Headers({ "user-agent": `models.dev/${VERSION}` }) + const headers = new Headers() for (const [key, value] of new Headers(options.headers)) headers.set(key, value) for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value) diff --git a/packages/sdk/src/effect/client.ts b/packages/sdk/src/effect/client.ts index b356a2813..0eb98835d 100644 --- a/packages/sdk/src/effect/client.ts +++ b/packages/sdk/src/effect/client.ts @@ -1,7 +1,6 @@ import { Context, Effect, Layer, Schema } from "effect" import { HttpClient, HttpClientResponse } from "effect/unstable/http" import type { Catalog, ModelMetadataMap, ProviderMap } from "../types.js" -import { VERSION } from "../version.js" /** The only error in the failure channel of client methods. Wraps the underlying `HttpClientError` as `cause`. */ export class ModelsDevError extends Schema.TaggedErrorClass()("ModelsDevError", { @@ -30,7 +29,7 @@ export const make = (options?: ClientOptions) => const get = (path: string): Effect.Effect => http .get(new URL(path, base), { - headers: { "user-agent": `models.dev/${VERSION}`, ...options?.headers }, + headers: options?.headers, }) .pipe( Effect.flatMap(HttpClientResponse.filterStatusOk), @@ -52,7 +51,7 @@ export const make = (options?: ClientOptions) => export type ModelsClient = Effect.Success> /** Service key for dependency-injecting a shared client: `yield* Models.Service`. */ -export class Service extends Context.Service()("models.dev/Models") {} +export class Service extends Context.Service()("@opencode-ai/models/Models") {} /** Layer providing `Models.Service`; requires an `HttpClient` in the environment. */ export const layer = (options?: ClientOptions) => Layer.effect(Service)(make(options)) diff --git a/packages/sdk/test/client.test.ts b/packages/sdk/test/client.test.ts index 993ef3f25..3f898ef91 100644 --- a/packages/sdk/test/client.test.ts +++ b/packages/sdk/test/client.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { Models, ModelsDevError, VERSION } from "../src/index.js" +import { Models, ModelsDevError } from "../src/index.js" interface Call { url: URL @@ -50,13 +50,13 @@ test("baseUrl with subpath is preserved, with or without trailing slash", async ]) }) -test("identifies itself with a versioned user-agent", async () => { +test("does not add headers by default", async () => { const { calls, fetch } = stub({}) await Models.make({ fetch }).providers() - expect(headers(calls[0]!).get("user-agent")).toBe(`models.dev/${VERSION}`) + expect([...headers(calls[0]!).entries()]).toEqual([]) }) -test("client headers override defaults, request headers override client headers", async () => { +test("request headers override client headers", async () => { const { calls, fetch } = stub({}) const client = Models.make({ fetch, headers: { "user-agent": "custom", "x-one": "client", "x-two": "client" } }) await client.providers({ headers: { "x-two": "request" } }) diff --git a/packages/sdk/test/effect.test.ts b/packages/sdk/test/effect.test.ts index b15120e1e..943acedbe 100644 --- a/packages/sdk/test/effect.test.ts +++ b/packages/sdk/test/effect.test.ts @@ -25,7 +25,7 @@ test("providers() succeeds through an injected transport", async () => { const result = await program.pipe(Effect.provide(layer), Effect.runPromise) expect(result["anthropic"]?.id).toBe("anthropic") expect(requests[0]?.url).toBe("https://models.dev/api.json") - expect(requests[0]?.headers.get("user-agent")).toMatch(/^models\.dev\//) + expect(requests[0]?.headers.get("user-agent")).toBeNull() }) test("models() and catalog() hit their endpoints, baseUrl subpath preserved", async () => {