From a9e68d06692e466d29b57c12154fadd72dd423cd Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 20:06:11 -0700 Subject: [PATCH 1/4] chore: use the Interchange LICENSE text and add the CLA --- .github/workflows/cla.yml | 53 ++++++++++++++++++++++++++++++++ CLA.md | 64 +++++++++++++++++++++++++++++++++++++++ LICENSE | 2 +- 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/cla.yml create mode 100644 CLA.md diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..a03ad39 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,53 @@ +name: CLA Assistant + +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, synchronize] + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +concurrency: + group: cla-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} + cancel-in-progress: true + +jobs: + cla: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Maintainer fast path + if: >- + github.event_name == 'pull_request_target' && + (contains(fromJSON('["TheGreatAxios","brianjfox"]'), github.event.pull_request.user.login) || + endsWith(github.event.pull_request.user.login, '[bot]')) + run: echo "Maintainer or bot pull request; CLA not required." + - name: CLA Assistant + if: >- + ((github.event.comment.body == 'recreate-signatures' || + github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || + github.event_name == 'pull_request_target') && + !(github.event_name == 'pull_request_target' && + (contains(fromJSON('["TheGreatAxios","brianjfox"]'), github.event.pull_request.user.login) || + endsWith(github.event.pull_request.user.login, '[bot]'))) + # corbitsdev/cla-assistant-action v2.6.1-node24: upstream v2.6.1 on node24. + uses: corbitsdev/cla-assistant-action@ef6d3e51db8232fe93090810f13bde30497c1d74 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + path-to-document: "https://github.com/${{ github.repository }}/blob/main/CLA.md" + path-to-signatures: "signatures/version1/cla.json" + branch: "cla-signatures" + allowlist: TheGreatAxios,brianjfox,*[bot] + custom-notsigned-prcomment: >- + Thank you for your contribution. Before it can be merged, please read our + [Contributor License Agreement](https://github.com/${{ github.repository }}/blob/main/CLA.md) + and sign it by posting a new comment on this pull request containing + exactly the line below (nothing else): + custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA" + custom-allsigned-prcomment: "All contributors have signed the CLA." diff --git a/CLA.md b/CLA.md new file mode 100644 index 0000000..201eec8 --- /dev/null +++ b/CLA.md @@ -0,0 +1,64 @@ +# Contributor License Agreement (CLA) + +Version 1.0 + +This Contributor License Agreement ("Agreement") is entered into between the contributor ("Contributor") and ABK Labs, Inc. ("Maintainer"). + +By submitting any Contribution to a project maintained by Maintainer, the Contributor agrees to the following terms. + +## 1. Definitions + +"Contribution" means any source code, object code, documentation, test cases, designs, specifications, bug fixes, enhancements, comments, pull requests, commits, issues containing code, or other materials intentionally submitted to a project maintained by Maintainer. + +## 2. Copyright Ownership + +The Contributor retains ownership of the Contributor's copyrights in the Contribution. + +No transfer of copyright ownership is required by this Agreement. + +## 3. License Grant to Maintainer + +The Contributor grants Maintainer a perpetual, worldwide, non-exclusive, irrevocable, royalty-free license to: + +- use, reproduce, modify, distribute, display, perform, and sublicense the Contribution; +- combine the Contribution with other software and works; +- distribute the Contribution under the project's current license; +- distribute the Contribution under future versions of the project's license; +- distribute the Contribution under alternative open-source, source-available, commercial, proprietary, or dual-license terms. + +## 4. License Grant to the Public + +The Contributor agrees that the Contribution may be distributed as part of the applicable project under the project's then-current license terms, including LGPL-2.1 or any successor license adopted by Maintainer. + +## 5. Contributor Representations + +The Contributor represents and warrants that: + +1. The Contributor created the Contribution or otherwise has sufficient rights to submit it. +2. The Contributor has the legal authority to grant the rights described in this Agreement. +3. To the best of the Contributor's knowledge, the Contribution does not knowingly infringe the intellectual property rights of any third party. +4. Any third-party material included in the Contribution has been properly disclosed and is compatible with the rights granted under this Agreement. + +## 6. Corporate Contributions + +If a Contribution is submitted on behalf of an employer or other legal entity, the person accepting this Agreement represents that they are authorized to bind that entity to this Agreement. + +## 7. No Obligation + +Maintainer is not obligated to use, distribute, maintain, support, or accept any Contribution. + +## 8. Disclaimer + +Except as expressly stated in this Agreement, the Contribution is provided "AS IS" without warranties or conditions of any kind. + +## 9. Electronic Acceptance + +The Contributor agrees that electronic acceptance of this Agreement, including acceptance through a website, source-control platform, click-through process, pull-request workflow, or similar mechanism, shall have the same force and effect as a handwritten signature. + +## 10. Governing Law + +This Agreement shall be governed by the laws of the State of California, excluding its conflict-of-law provisions. + +--- + +By submitting a Contribution, the Contributor acknowledges that they have read and agree to this Agreement. diff --git a/LICENSE b/LICENSE index 173c3a2..c6487f4 100644 --- a/LICENSE +++ b/LICENSE @@ -159,7 +159,7 @@ To apply these terms, attach the following notices to the library. It is safest one line to give the library's name and an idea of what it does. Copyright (C) year name of author - This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License. + This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. From 072fb3efe6c4a530546291a32535e70e9725db26 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 20:06:12 -0700 Subject: [PATCH 2/4] build: align package.json and tsconfig with the package standard --- .gitignore | 5 +++-- package.json | 15 ++++++++++----- tsconfig.build.json | 11 +++-------- tsconfig.json | 7 +++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 2adc687..6f17218 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ node_modules/ dist/ +*.tsbuildinfo *.tgz -.corbits/ -.DS_Store +coverage/ .env .env.* +.DS_Store diff --git a/package.json b/package.json index 2ce3271..93b7a19 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,14 @@ "name": "@corbits/ollama-adapter", "version": "0.2.0", "description": "Ollama as an Interchange inference provider: OpenAI-compatible and Anthropic messages factories, one reasoning setting, think-tag stripping, inline tool JSON repair.", - "homepage": "https://github.com/corbitsdev/corbits-ollama-adapter", + "keywords": [ + "corbits", + "inference", + "interchange", + "llm", + "ollama" + ], + "homepage": "https://github.com/corbitsdev/corbits-ollama-adapter#readme", "bugs": { "url": "https://github.com/corbitsdev/corbits-ollama-adapter/issues" }, @@ -19,8 +26,6 @@ ], "type": "module", "sideEffects": false, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", @@ -31,13 +36,13 @@ "access": "public" }, "scripts": { - "build": "tsc -p tsconfig.build.json", + "build": "rm -rf dist && tsc -p tsconfig.build.json", "prepack": "bun run build", "typecheck": "tsc --noEmit", "lint": "oxlint", "format": "oxfmt", "format:check": "oxfmt --check", - "test": "bun test src", + "test": "bun test src --pass-with-no-tests", "test:e2e": "bun test e2e", "check": "bun run typecheck && bun run lint && bun run format:check && bun run test" }, diff --git a/tsconfig.build.json b/tsconfig.build.json index cc96555..af6694f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,20 +1,15 @@ { - // Publish build: emits dist/ (js + d.ts) for the packed tarball. - // Source stays the dev surface (bun test, typecheck); dist is what - // the `default`/`types` export conditions resolve to once packed. "extends": "./tsconfig.json", "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "allowImportingTsExtensions": false, - "composite": false, "noEmit": false, "declaration": true, "declarationMap": false, "sourceMap": false, + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": "src" }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "dist", "node_modules"] + "exclude": ["src/**/*.test.ts"] } diff --git a/tsconfig.json b/tsconfig.json index b883ac9..dda942c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,17 +3,16 @@ "esModuleInterop": true, "skipLibCheck": true, "target": "ESNext", - "allowJs": true, - "resolveJsonModule": true, + "module": "ESNext", + "moduleResolution": "bundler", "moduleDetection": "force", "isolatedModules": true, "verbatimModuleSyntax": true, - "moduleResolution": "bundler", + "resolveJsonModule": true, "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "exactOptionalPropertyTypes": true, - "module": "ESNext", "noEmit": true, "lib": ["ESNext"], "types": ["bun"] From 52a7843b2b989a5e32c8e1536c8d57b9c74d28ae Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 20:06:12 -0700 Subject: [PATCH 3/4] ci: run check and a Node pack smoke --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0919147 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: + pull_request: + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun run check + - name: node consumer smoke + run: | + set -euo pipefail + TARBALL="$PWD/$(npm pack --silent)" + mkdir -p "$RUNNER_TEMP/c" && cd "$RUNNER_TEMP/c" + npm init -y >/dev/null && npm pkg set type=module >/dev/null + npm install "$TARBALL" + node -e 'import("@corbits/ollama-adapter").then((m) => { for (const n of ["createOllamaAdapter", "createOllamaAnthropicAdapter"]) if (typeof m[n] !== "function") throw new Error("missing export: " + n); })' From 39944f4143c8094d47d9433294ddbc73b43f7419 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 20:06:12 -0700 Subject: [PATCH 4/4] docs: trim CONTRIBUTING, add AGENTS.md and a README quickstart --- AGENTS.md | 28 ++++++++++++++++++++++++++++ CONTRIBUTING.md | 34 +++++++++++++++++++++++----------- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4d73733 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# AGENTS.md + +## Purpose + +`@corbits/ollama-adapter` provides `@intx/inference` adapter factories for Ollama's OpenAI-compatible `/v1/chat/completions` and Anthropic-compatible `/v1/messages` surfaces. It owns the request overrides (`maxOutputTokens`, `reasoning`) and the stream repairs Ollama needs on `/v1/chat/completions`. It does not own SSE parsing or message marshaling (the wrapped Interchange adapters) or Ollama's native `/api` endpoints. + +## Layout + +- `src/adapter.ts`: `createOllamaAdapter` (wraps the OpenAI Chat Completions adapter) and `createOllamaAnthropicAdapter` (wraps the Anthropic adapter on `/messages`). +- `src/overrides.ts`: the `OllamaAdapterConfig`, `OllamaAdapterOverride` and `Reasoning` schemas, legacy-key migration and per-model resolution. +- `src/think-tags.ts`: reclassifies inline `` text into thinking deltas. +- `src/inline-tool-json.ts`: reclassifies a tool call emitted as JSON text into tool-call events. +- `src/index.ts`: the public entry (the two factories and three schemas). +- `src/*.test.ts`: unit tests next to the module they cover. +- `e2e/`: the inference-harness suite and the live suites gated on `OLLAMA_BASE_URL`. + +## Rules + +- Consume `@intx/inference`, `@intx/log` and `@intx/types` as `peerDependencies` (`^0.4.0`), never vendored. A host must resolve exactly one copy. +- Parse the `quirks` bag with arktype; unknown override keys are rejected. +- `exactOptionalPropertyTypes` is on: omit optional keys, never assign `undefined` to them. +- Leave the wrapped adapters' parsing unmodified; repairs apply only on the OpenAI-compatible surface, since Ollama's Anthropic surface already emits native `thinking` and `tool_use` blocks. + +## Local development + +```sh +bun install && bun run check +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8487a7f..f4720df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,22 +1,21 @@ # Contributing -## Packaging - -Bun >= 1.2 and Node >= 24 are the engines floors. The packed tarball ships compiled `dist/` (js + d.ts) built by `bun run build` (also wired as `prepack`); both Bun and native Node resolve the package through `dist` (`main`/`types` plus the `.` export's `default`/`types` conditions). - -## How it works - -`createOllamaAdapter` reuses Interchange's built-in OpenAI Chat Completions adapter, then overlays max-tokens and `reasoning_effort`, and repairs think-tags / inline tool JSON that Ollama emits on `/v1/chat/completions`. `createOllamaAnthropicAdapter` wraps the stock Anthropic adapter, rewrites the path to `/messages`, maps `reasoning` onto `thinking`, and swaps `x-api-key` for the bearer credential sentinel. Ollama's Anthropic surface already emits native `thinking` and `tool_use` blocks, so that path does no think-tag stripping or JSON salvage. - ## Development ```sh bun install -bun run check # typecheck + lint + format:check + unit tests -bun run test:e2e # inference-harness and live Ollama suites +bun run check ``` -`e2e/harness.test.ts` drives the adapter through `@intx/inference-testing` and always runs. The live suites run only when `OLLAMA_BASE_URL` names an Ollama server (for example `http://host:11434`) and skip otherwise. `e2e/live-ollama.test.ts` drives both factories with model `OLLAMA_MODEL` (default `gpt-oss:20b`); `e2e/reasoning-live.test.ts` checks `reasoning` on both surfaces with model `OLLAMA_REASONING_MODEL` (default `qwen3:8b`). +`bun run check` runs typecheck, lint, format check and unit tests. `bun run format` rewrites the tree. + +Contributors sign the [CLA](CLA.md) on their first PR; the CLA bot explains how. + +`bun run test:e2e` runs the inference-harness and live Ollama suites. `e2e/harness.test.ts` drives the adapter through `@intx/inference-testing` and always runs. The live suites run only when `OLLAMA_BASE_URL` names an Ollama server (for example `http://host:11434`) and skip otherwise. `e2e/live-ollama.test.ts` drives both factories with model `OLLAMA_MODEL` (default `gpt-oss:20b`); `e2e/reasoning-live.test.ts` checks `reasoning` on both surfaces with model `OLLAMA_REASONING_MODEL` (default `qwen3:8b`). + +## How it works + +`createOllamaAdapter` reuses Interchange's built-in OpenAI Chat Completions adapter, then overlays max-tokens and `reasoning_effort`, and repairs think-tags / inline tool JSON that Ollama emits on `/v1/chat/completions`. `createOllamaAnthropicAdapter` wraps the stock Anthropic adapter, rewrites the path to `/messages`, maps `reasoning` onto `thinking`, and swaps `x-api-key` for the bearer credential sentinel. Ollama's Anthropic surface already emits native `thinking` and `tool_use` blocks, so that path does no think-tag stripping or JSON salvage. ## Commit messages @@ -24,3 +23,16 @@ Commit subjects and PR titles follow [Conventional Commits](https://www.conventi Add `!` only for public API breaks: removed or renamed exports, changed signatures, newly required params. Peer and dependency range changes are `build(deps):` with no `!`. Keep subjects imperative, lowercase after the colon, 72 characters or less, and free of ticket IDs. Every PR links its issue with a `Closes ` line in the PR body. + +## Releasing + +Releases are manual. On a clean, up-to-date `main`: + +```sh +npm version -m "chore(release): %s" +git push --follow-tags +gh release create "v$(node -p 'require("./package.json").version')" --generate-notes +npm publish +``` + +Bump minor only for breaking API changes; everything else is a patch. `prepack` builds `dist/` from the tagged commit. diff --git a/README.md b/README.md index 1426689..1362f02 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,49 @@ bun add @corbits/ollama-adapter @intx/inference@^0.4.0 @intx/log@^0.4.0 @intx/ty Runs on Bun >= 1.2 or Node >= 24. +## Quickstart + +Needs Ollama running locally with the model pulled (`ollama pull gpt-oss:20b`). + +```ts +import { createDependencies, runInference } from "@intx/inference"; +import { createOllamaAdapter } from "@corbits/ollama-adapter"; + +const deps = createDependencies({ + has: (provider) => provider === "ollama", + resolve: (source, quirks) => createOllamaAdapter(source, quirks), +}); + +let seq = 0; +for await (const event of runInference({ + deps, + source: { + id: "ollama", + provider: "ollama", + baseURL: "http://localhost:11434/v1", + credentialId: "ollama", + model: "gpt-oss:20b", + quirks: { default: { reasoning: "low" } }, + }, + turns: [ + { + role: "user", + timestamp: Date.now(), + content: [{ type: "text", text: "Say hello." }], + }, + ], + nextSeq: () => seq++, + // Local Ollama ignores the key; Ollama Cloud needs a real one. + readMaterial: () => ({ secret: "ollama" }), +})) { + if (event.type === "inference.text.delta") + process.stdout.write(event.data.token); + if (event.type === "inference.error") + throw new Error(event.data.error.message); +} +process.stdout.write("\n"); +``` + ## Where it fits [Interchange](https://github.com/faremeter/interchange) runs AI agents as principals (accounts that hold their own identity, permissions and credentials). Corbits packages add what an agent product needs around it.