{"_id":"@costa-app/openclaw-costa-user-attribution","name":"@costa-app/openclaw-costa-user-attribution","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.1":{"name":"@costa-app/openclaw-costa-user-attribution","version":"0.1.1","private":false,"description":"OpenClaw provider plugin for the Costa LLM gateway. Stamps a per-agent X-COSTA-USER-ATTRIBUTION header onto outbound model requests so one shared gateway key can be attributed per user.","type":"module","license":"BSD-3-Clause","author":{"name":"Costa Security"},"homepage":"https://github.com/costa-app/openclaw-costa-user-attribution#readme","repository":{"type":"git","url":"git+https://github.com/costa-app/openclaw-costa-user-attribution.git"},"bugs":{"url":"https://github.com/costa-app/openclaw-costa-user-attribution/issues"},"keywords":["openclaw","openclaw-plugin","llm","gateway","attribution"],"scripts":{"build":"tsc -p tsconfig.json","test":"vitest run","test:watch":"vitest","fake-openai":"node scripts/fake-openai.mjs","prepublishOnly":"npm run build"},"peerDependencies":{"openclaw":">=2026.6.8"},"devDependencies":{"@types/node":"^22.0.0","openclaw":"2026.6.9","typescript":"^5.6.0","vitest":"^2.0.0"},"openclaw":{"extensions":["./dist/index.js"],"runtimeExtensions":["./dist/index.js"],"compat":{"pluginApi":">=2026.6.8","minGatewayVersion":"2026.6.8"},"build":{"openclawVersion":"2026.6.8","pluginSdkVersion":"2026.6.8"}},"gitHead":"2cc31882410512c634c6736a61d1e112ee34c6d6","_id":"@costa-app/openclaw-costa-user-attribution@0.1.1","_nodeVersion":"24.11.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-Qt9qoQMnQf0KQhEQcxdM7x4m+eoOnBMUVnFUeeuGhREP0APHlu8woo4X+gVis3QdxwgpFT5OxnSh1MHEB2z3SQ==","shasum":"a965003caeadcd34b7e564353ee3f0732ec8e9ed","tarball":"https://registry.npmjs.org/@costa-app/openclaw-costa-user-attribution/-/openclaw-costa-user-attribution-0.1.1.tgz","fileCount":7,"unpackedSize":16654,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICpxU01vaFMTYI9Mkv1hV2TvpSxFmJl5t6gZCr0OU1hXAiEAisdZrWVjNW6kL7h5ntHu1pfh2VBZboU4P4z/cUvWkws="}]},"_npmUser":{"name":"heimark","email":"jake@heimark.org"},"directories":{},"maintainers":[{"name":"heimark","email":"jake@heimark.org"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/openclaw-costa-user-attribution_0.1.1_1783532775447_0.4331927210732609"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-08T17:46:15.248Z","0.1.1":"2026-07-08T17:46:15.569Z","modified":"2026-07-08T17:46:15.885Z"},"maintainers":[{"name":"heimark","email":"jake@heimark.org"}],"description":"OpenClaw provider plugin for the Costa LLM gateway. Stamps a per-agent X-COSTA-USER-ATTRIBUTION header onto outbound model requests so one shared gateway key can be attributed per user.","homepage":"https://github.com/costa-app/openclaw-costa-user-attribution#readme","keywords":["openclaw","openclaw-plugin","llm","gateway","attribution"],"repository":{"type":"git","url":"git+https://github.com/costa-app/openclaw-costa-user-attribution.git"},"author":{"name":"Costa Security"},"bugs":{"url":"https://github.com/costa-app/openclaw-costa-user-attribution/issues"},"license":"BSD-3-Clause","readme":"# OpenClaw Costa User Attribution\n\nAn [OpenClaw](https://github.com/openclaw/openclaw) **provider plugin** that lets\none shared gateway API key carry **per-agent user attribution**. When an agent has\na `costa-user` value, every model request it makes is stamped with\n`X-COSTA-USER-ATTRIBUTION: <that value>`, so the gateway can attribute traffic per\nuser without issuing one API key per user.\n\n- **npm:** `@costa-app/openclaw-costa-user-attribution`\n- **plugin id / provider id:** `openclaw-costa-user-attribution`\n- **Requires:** OpenClaw `>= 2026.6.8`\n\n## Why this exists\n\nOpenClaw has no per-agent header seam: custom model-request headers are only\nconfigurable per *provider* or per *model*, applied to every agent that uses them.\nThe provider-owned `wrapStreamFn` hook is the only place that sees both the active\n`agentId` and the config, so this plugin uses it to read a static per-agent field\nand inject the header.\n\n## Install\n\n```bash\nopenclaw plugins install npm:@costa-app/openclaw-costa-user-attribution\n```\n\nThen configure the provider and per-agent attribution (below) and restart the\ngateway: `openclaw gateway restart`.\n\nConfirm it loaded:\n\n```bash\nopenclaw plugins inspect openclaw-costa-user-attribution --runtime --json\n```\n\n## Configure\n\nBoth `baseUrl` **and** `models[]` are required. A gateway proxies to whatever\nupstream models you configured, so the plugin ships no model list and refuses to\nregister without one (fail closed, no placeholders). The shared key is read from\n`COSTA_USER_ATTRIBUTION_API_KEY` (or set `apiKey` directly). Model ids become\nselectable as `openclaw-costa-user-attribution/<id>`.\n\n```json5\n// openclaw.json\n{\n  models: {\n    providers: {\n      \"openclaw-costa-user-attribution\": {\n        baseUrl: \"https://api.somegateway.com/v1\",       // REQUIRED — no default host\n        apiKey: \"$COSTA_USER_ATTRIBUTION_API_KEY\",        // one shared key for everyone\n        models: [                                         // REQUIRED — your gateway's models\n          {\n            id: \"gpt-4o\",\n            name: \"GPT-4o\",\n            reasoning: false,\n            input: [\"text\", \"image\"],\n            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n            contextWindow: 128000,\n            maxTokens: 16000,\n          },\n        ],\n      },\n    },\n  },\n  agents: {\n    list: [\n      {\n        id: \"alice\",\n        model: \"openclaw-costa-user-attribution/gpt-4o\",\n        params: { \"costa-user\": \"alice@costa.security\" }, // per-agent attribution\n      },\n      {\n        id: \"bob\",\n        model: \"openclaw-costa-user-attribution/gpt-4o\",\n        params: { \"costa-user\": \"bob@costa.security\" },\n      },\n    ],\n  },\n}\n```\n\nEach model entry requires `id`, `name`, `reasoning`, `input`, `cost` (all four\nkeys), `contextWindow`, and `maxTokens`. Set `cost` to your real per-million-token\npricing if you want accurate accounting; zeros are fine otherwise.\n\n## How it works\n\n- Registers an `openai-completions` provider with id\n  `openclaw-costa-user-attribution`.\n- `baseUrl` is **required** from operator config — there is no default host. If it\n  is missing, provider discovery fails with a clear error (fail closed).\n- `wrapStreamFn` and `wrapSimpleCompletionStreamFn` read\n  `agents.list[].params[\"costa-user\"]` for the active agent. Present → header\n  injected (merged with any existing headers). Absent/blank → request passes\n  through unchanged.\n\nThe header name `X-COSTA-USER-ATTRIBUTION` and the agent param `costa-user` are the\ncontract your gateway reads; they are independent of the plugin/package name.\n\n## Develop\n\n```bash\nnpm install\nnpm run build     # tsc -> dist/index.js (the published runtime entry)\nnpm test          # unit + SSE integration\n```\n\n- `attribution.test.ts` — unit-tests header injection / passthrough / merge.\n- `provider-catalog.test.ts` — unit-tests the fail-closed catalog (required\n  `baseUrl` + `models[]`, `null` on missing key).\n- `index.e2e.test.ts` — drives a real `openai-completions` turn through an\n  ephemeral SSE server and asserts the header on the wire.\n\n`package.json` declares both `openclaw.extensions` and `openclaw.runtimeExtensions`\nas `./dist/index.js` — the published package ships compiled JS only, not\nTypeScript sources. `npm run build` runs automatically on `prepublishOnly` so\n`dist/` is always fresh before publish.\n\n### Manual smoke test against a fake gateway\n\n```bash\nnpm run fake-openai   # logs X-COSTA-USER-ATTRIBUTION, serves SSE on :8099\n```\n\nPoint `baseUrl` at `http://127.0.0.1:8099/v1`, send a message as `alice`, and the\nfake server logs `x-costa-user-attribution: alice@costa.security`.\n\n## Publish\n\n```bash\nnpm publish --access public\n```\n\n`prepublishOnly` builds `dist/` first; `files` ships only `dist/`,\n`openclaw.plugin.json`, the README, and the license — no TypeScript sources.\n\n### Release process\n\nReleases are manual — this package doesn't ship often enough to warrant an\nautomated release workflow. To cut one:\n\n1. Merge your changes to `main`.\n2. Let CI finish on `main` (GitHub Actions → CI) — don't publish on a red run.\n3. `npm version patch` (or `minor`/`major`) — bumps `package.json` and creates\n   a `vX.Y.Z` tag.\n4. `git push --follow-tags`\n5. `npm publish --access public`\n\n## Files\n\n| File | Role |\n| --- | --- |\n| `index.ts` | Plugin entry: registers the provider, custom catalog `run` (required baseUrl), both wrap hooks. Compiles to `dist/index.js`, the published runtime entry. |\n| `attribution.ts` | `buildAttributionWrapper` — reads `params[\"costa-user\"]`, stamps the header. |\n| `provider-catalog.ts` | `resolveCostaCatalog` (fail-closed baseUrl + models check) + `buildCostaProvider`. |\n| `openclaw.plugin.json` | Plugin manifest (id, provider, auth choice, config schema). |\n| `attribution.test.ts` | Unit tests for the wrapper. |\n| `provider-catalog.test.ts` | Unit tests for the catalog resolution. |\n| `index.e2e.test.ts` | SSE integration test. |\n| `scripts/fake-openai.mjs` | Standalone synthetic gateway for manual runs. |\n\nThese `.ts` sources and tests live in the repo for development but are not part\nof the published npm package.\n\n## License\n\nBSD-3-Clause. See [LICENSE](./LICENSE).\n","readmeFilename":"README.md","_rev":"1-9c79f0e832626c9e3006c5535204bdb6"}