Files
PaperClipAI/server/src/__tests__/fast-response-provider.test.ts
T
DottaandPaperclip 9179d8dcc3 fix: credit Paperclip for managed OpenRouter requests (#16850)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - AI connections choose credentials and routes independently of the
agent harness.
> - OpenRouter assigns app credit from the final request headers.
> - Harness defaults can assign Paperclip work to the underlying
harness.
> - This pull request applies one public Paperclip identity to managed
OpenRouter requests.
> - The benefit is consistent app credit across supported harnesses and
internal model calls.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

App attribution for existing OpenRouter connections and explicit
provider configurations.

**Current behavior**

Requests can omit attribution or carry the harness identity. Paperclip
does not receive consistent credit in OpenRouter rankings.

**Proposed behavior**

Use `https://paperclip.ing`, the title `Paperclip`, and the coding
categories `cloud-agent,programming-app` for managed requests to
OpenRouter. Keep credentials, routing, and unrelated providers intact.

**Reason and benefit**

The provider URL identifies the app across personal and shared API keys.
A search of public issues and pull requests found no duplicate
attribution work. This improves existing AI connections and does not add
a separate roadmap feature.

## What Changed

- Add a shared attribution policy and a parity-checked copy for the
standalone Runner.
- Apply it to managed Codex, Claude, OpenCode, and Hermes connections,
plus native Pi and explicit legacy provider configs.
- Preserve attribution across Codex config filtering, Claude environment
filtering, OpenCode proxy requests, and model-level overrides.
- Attribute direct fast-response and decision-model requests, including
custom connections to the OpenRouter endpoint.
- Add positive and negative regression tests and provider attribution
requirements to the connector playbook.
- Advance native Pi to profile 24, preserving historical identities and
binding the attribution source in its declaration and release checks.

## Verification

- Final head `6df282d2a58c2698e41127443903d6fc85b4b72f`: all CI/status
checks passed or were intentionally skipped; Greptile is **5/5**, with
zero unresolved review threads. [Final CI
run](https://github.com/paperclipai/paperclip/actions/runs/39274938069).
- Focused server/shared/config suites: 132 tests passed before the final
edge-case additions. Expanded adapter suites pass 48 tests; the final
OpenCode merge passes 15 tests; global/EU/US endpoint and policy-parity
suites pass 26 tests.
- Focused Runner suites: 85 tests passed, including model overrides and
recovery/config boundaries. Profile installation/environment/config
tests subsequently passed 72 tests (one optional distribution test
skipped); historical decoding passed 11 tests and updated recovery
coverage passed 27 tests. The Rust profile admission test and all five
native Pi launch/recovery tests passed.
- All 114 managed AI connection tests pass together. They include
custom-header preservation for legacy and native Claude runs, both
OpenRouter and other gateways, inherited headers, and unresolved secret
references.
- Actual requests from installed Codex 0.160.0, Claude Code 2.1.245,
OpenCode 1.18.34, Pi 0.74.0 and 1.0.0, and the managed Hermes runtime
carried all four Paperclip headers. A local endpoint used dummy
credentials and returned an intentional 401 after capture. Codex and
Claude retry requests retained attribution. This proves outgoing header
precedence; it is not a successful model-inference test. The first
Claude capture had an argument-separation error and sent no request;
correcting the command produced the captured requests.
- A live request through `fastResponseModel` completed with `attribution
verified`. OpenRouter recorded generation
`gen-1792420273-Ez1GUTCc7YuZ1FJMCGob` under app ID `3118063`, at a cost
of $0.00000375. The public app record maps that ID to
`https://paperclip.ing/`.
- Full `pnpm -r typecheck` and `pnpm build` passed, including a repeat
after the review fixes. The final server build stamp records commit
`6df282d2a58c2698e41127443903d6fc85b4b72f`. `pnpm check:token-gates`
passed.
- CI first caught the stale Pi source attestation; it is now a new
immutable profile. Greptile caught custom-header loss, now fixed and
covered by the complete managed-connection suite. The next CI run caught
the missing Docker allowlist entry for profile 24; the allowlist and
release smoke pins are now synchronized. Local Docker verification
cannot run because its daemon socket is absent; The exact Docker context
check passed in CI on the final head.
- The initial local `pnpm test:run` did **not** pass. It ran across
source/build edits, reported chat/tool-access collection failures, three
workspace-runtime failures, two new managed-header failures, one Dot
Runner readiness failure, and two native-session-resume failures, and
was interrupted after the final-head CI server coverage passed. Its
secrets suite also reported 91 cases skipped. The isolated tool-access
suite passed all 428 tests, and the complete managed-connection suite
passed all 114 tests. These retries establish recovery, not the cause of
the original local failures; no claim of a successful full local run is
made. Final acceptance uses immutable final-head CI plus the focused
local and live checks above.
- Final-head CI encountered one unchanged UI assertion in
`FileViewerSheet.copy.test.tsx` (missing “Copied link” feedback after
clipboard completion). That shard passed on the earlier head with
identical UI code, and all six clipboard tests passed locally. A timing
race around the asynchronous React update is suspected but not
conclusively established. The single CI retry passed, with no
product/test code changed for it. The earlier failure remains part of
this verification record.

## Risks

- Native Pi profile 24 requires a fresh provider session when
encountering a profile 23 identity; recovery tests cover that boundary.
- Harness upgrades can change header precedence. The playbook requires
actual request capture when changing provider paths.
- Externally managed agents, unmanaged harness settings, and gateways
that replace headers need separate qualification. This change does not
intercept arbitrary model calls made by task scripts.
- The existing OpenRouter entry for the Paperclip URL still displays an
old title, “PrettyFly Opus Testing,” and no categories on the final
public-page recheck. The live request received credit under that URL's
app ID. Public title propagation and leaderboard placement remain
controlled by OpenRouter.
- Global, EU, and US OpenRouter hosts are recognized explicitly; other
subdomains are not treated as OpenRouter. No database migration,
credential ownership change, or Paperclip telemetry event is involved.
Attribution contains public product constants only.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository editing, shell
execution, web research, and live API verification. The exact deployment
identifier and context-window size are not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass — the focused suites listed
above; the unsuccessful full local attempt is explicitly recorded, with
full final-head coverage supplied by CI
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-19 10:28:53 -05:00

474 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from "vitest";
import type { AiConnectionMetadata } from "@paperclipai/shared";
import {
fastResponsePrompt,
fastResponseReceipt,
runFastResponseProvider,
} from "../services/fast-response-provider.js";
const ack =
"I’ll check the settings panel’s border styling and work on the fix.";
const chatResponse = () => ({
id: "receipt-1",
model: "openai/gpt-oss-120b",
created: 1,
choices: [
{
index: 0,
finish_reason: "stop",
message: { role: "assistant", content: ack },
},
],
usage: {
prompt_tokens: 100,
completion_tokens: 15,
total_tokens: 115,
cost: 0.00001,
},
});
const response = (body: unknown) =>
new Response(JSON.stringify(body), {
headers: { "content-type": "application/json" },
});
const input = (metadata: AiConnectionMetadata) => ({
metadata,
model: "test-model",
credential: "fixture-key",
prompt: fastResponsePrompt({ agentName: "Alex", message: "Fix the border" }),
signal: AbortSignal.timeout(3000),
});
const connections: AiConnectionMetadata[] = [
...(["openrouter", "openai", "anthropic", "google", "xai"] as const).map(
(provider) => ({ provider, method: "api_key" as const }),
),
{
provider: "anthropic",
method: "api_key",
routing: {
kind: "bedrock",
protocol: "bedrock",
region: "us-west-2",
auth: "bearer",
models: [],
},
},
...(["chat", "responses", "messages"] as const).map((protocol) => ({
provider: "openai" as const,
method: "api_key" as const,
routing: {
kind: "gateway" as const,
protocol,
baseUrl: "https://gateway.example/v1",
auth: "bearer" as const,
models: [],
},
})),
{
provider: "openai",
method: "api_key",
routing: {
kind: "local",
protocol: "chat",
baseUrl: "http://localhost:8080/v1",
auth: "none",
models: [],
},
},
...(["responses", "messages"] as const).map((protocol) => ({
provider: "openrouter" as const,
method: "api_key" as const,
routing: {
kind: "openrouter" as const,
protocol,
auth: "bearer" as const,
models: [],
},
})),
];
describe("fast response provider", () => {
it("rejects copied JSON output while retaining the provider charge", async () => {
const body = chatResponse();
body.choices[0].message.content = '{"message":"I’ll check the border."}';
const result = await runFastResponseProvider(input({ provider: "openrouter", method: "api_key" }), {
fetch: vi.fn(async () => response(body)),
});
expect(result.errorCode).toBe("invalid_output");
expect(result.text).toBeUndefined();
expect(result.receipt.costStatus).toBe("reported");
});
it("bounds unicode input and drops old context first", () => {
const prompt = fastResponsePrompt({
agentName: "Alex",
message: "🦄".repeat(10000),
recent: ["OLD-CONTEXT".repeat(1000)],
});
expect(Buffer.byteLength(prompt)).toBeLessThanOrEqual(4096);
expect(prompt).not.toContain("OLD-CONTEXT");
expect(prompt).toContain("[truncated]");
});
it("bounds escaped metadata and multibyte attachment names without losing the current request", () => {
const prompt = fastResponsePrompt({
agentName: "\u0001".repeat(100),
title: "🦄".repeat(200),
attachments: Array(5).fill("\u0001".repeat(80)),
message: "Fix the border",
recent: ["old".repeat(2000)],
});
expect(Buffer.byteLength(prompt)).toBeLessThanOrEqual(4096);
expect(prompt).toContain("Fix the border");
});
it("calls OpenRouter through the SDK with latency routing and records the charge", async () => {
const fetch = vi.fn(async () => response(chatResponse()));
const result = await runFastResponseProvider(
input({ provider: "openrouter", method: "api_key" }),
{ fetch },
);
expect(result.text).toBe(ack);
expect(result.receipt).toMatchObject({
inputTokens: 100,
outputTokens: 15,
costStatus: "reported",
costCents: "0.0010000",
});
const [url, init] = fetch.mock.calls[0] as unknown as [string, RequestInit];
expect(url).toBe("https://openrouter.ai/api/v1/chat/completions");
expectAttribution(url, init);
const body = JSON.parse(String(init.body));
expect(body).toMatchObject({
model: "test-model",
max_tokens: 256,
provider: { sort: "latency" },
});
expect(body.tools).toBeUndefined();
expect(fetch).toHaveBeenCalledTimes(1);
});
it.each([
[
{
provider: "openai",
method: "api_key",
routing: {
kind: "gateway",
protocol: "chat",
baseUrl: "https://gateway.example/v1",
auth: "bearer",
models: [],
},
},
"https://gateway.example/v1/chat/completions",
"Bearer fixture-key",
],
[
{
provider: "openai",
method: "api_key",
routing: {
kind: "local",
protocol: "chat",
baseUrl: "http://localhost:8080/v1",
auth: "none",
models: [],
},
},
"http://localhost:8080/v1/chat/completions",
null,
],
] as const)(
"honors saved chat endpoint/auth %j",
async (metadata, expectedUrl, auth) => {
const fetch = vi.fn(async () => response(chatResponse()));
const result = await runFastResponseProvider(
input(metadata as AiConnectionMetadata),
{ fetch },
);
expect(result.text).toBe(ack);
const [url, init] = fetch.mock.calls[0] as unknown as [
string,
RequestInit,
];
expect(url).toBe(expectedUrl);
expectAttribution(url, init);
expect(new Headers(init.headers).get("authorization")).toBe(auth);
},
);
it.each([
[
{ provider: "openai", method: "api_key" },
"https://api.openai.com/v1/responses",
],
[{ provider: "xai", method: "api_key" }, "https://api.x.ai/v1/responses"],
[
{
provider: "openai",
method: "api_key",
routing: {
kind: "openrouter",
protocol: "responses",
auth: "bearer",
models: [],
},
},
"https://openrouter.ai/api/v1/responses",
],
[
{
provider: "openai",
method: "api_key",
routing: {
kind: "gateway",
protocol: "responses",
baseUrl: "https://gateway.example/v1",
auth: "bearer",
models: [],
},
},
"https://gateway.example/v1/responses",
],
] as const)("supports Responses %j", async (metadata, url) => {
const fetch = vi.fn(async () =>
response({
id: "resp_1",
object: "response",
created_at: 1,
model: "test-model",
status: "completed",
output: [
{
type: "message",
id: "msg_1",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: ack, annotations: [] }],
},
],
usage: { input_tokens: 100, output_tokens: 15, total_tokens: 115 },
}),
);
const result = await runFastResponseProvider(
input(metadata as AiConnectionMetadata),
{ fetch },
);
expect(result.text).toBe(ack);
expect(result.receipt).toMatchObject({
inputTokens: 100,
outputTokens: 15,
});
const [actual, init] = fetch.mock.calls[0] as unknown as [
string,
RequestInit,
];
expect(actual).toBe(url);
expect(new Headers(init.headers).get("authorization")).toBe(
"Bearer fixture-key",
);
expect(JSON.parse(String(init.body))).toMatchObject({
model: "test-model",
max_output_tokens: 256,
});
});
it.each([
[
{ provider: "anthropic", method: "api_key" },
"https://api.anthropic.com/v1/messages",
"x-api-key",
"fixture-key",
],
[
{
provider: "anthropic",
method: "api_key",
routing: {
kind: "openrouter",
protocol: "messages",
auth: "bearer",
models: [],
},
},
"https://openrouter.ai/api/v1/messages",
"authorization",
"Bearer fixture-key",
],
[
{
provider: "anthropic",
method: "api_key",
routing: {
kind: "gateway",
protocol: "messages",
baseUrl: "https://messages.example/v1",
auth: "bearer",
models: [],
},
},
"https://messages.example/v1/messages",
"authorization",
"Bearer fixture-key",
],
] as const)("supports Messages %j", async (metadata, url, header, key) => {
const fetch = vi.fn(async () =>
response({
id: "msg_1",
type: "message",
role: "assistant",
model: "test-model",
content: [{ type: "text", text: ack }],
stop_reason: "end_turn",
stop_sequence: null,
usage: { input_tokens: 100, output_tokens: 15 },
}),
);
const result = await runFastResponseProvider(
input(metadata as AiConnectionMetadata),
{ fetch },
);
expect(result.text).toBe(ack);
expect(result.receipt).toMatchObject({
inputTokens: 100,
outputTokens: 15,
});
const [actual, init] = fetch.mock.calls[0] as unknown as [
string,
RequestInit,
];
expect(actual).toBe(url);
expect(new Headers(init.headers).get(header)).toBe(key);
expect(JSON.parse(String(init.body))).toMatchObject({
model: "test-model",
max_tokens: 256,
});
});
it("supports Google generation and auth", async () => {
const fetch = vi.fn(async () =>
response({
candidates: [
{
content: { role: "model", parts: [{ text: ack }] },
finishReason: "STOP",
index: 0,
},
],
usageMetadata: {
promptTokenCount: 100,
candidatesTokenCount: 15,
totalTokenCount: 115,
},
}),
);
const result = await runFastResponseProvider(
input({ provider: "google", method: "api_key" }),
{ fetch },
);
expect(result.text).toBe(ack);
expect(result.receipt).toMatchObject({
inputTokens: 100,
outputTokens: 15,
});
const [url, init] = fetch.mock.calls[0] as unknown as [string, RequestInit];
expect(url).toBe(
"https://generativelanguage.googleapis.com/v1beta/models/test-model:generateContent",
);
expect(new Headers(init.headers).get("x-goog-api-key")).toBe("fixture-key");
expect(JSON.parse(String(init.body)).generationConfig.maxOutputTokens).toBe(
256,
);
});
it("supports Bedrock region, bearer key and Converse", async () => {
const fetch = vi.fn(async () =>
response({
output: { message: { role: "assistant", content: [{ text: ack }] } },
stopReason: "end_turn",
usage: { inputTokens: 100, outputTokens: 15, totalTokens: 115 },
metrics: { latencyMs: 50 },
}),
);
const result = await runFastResponseProvider(
input({
provider: "anthropic",
method: "api_key",
routing: {
kind: "bedrock",
protocol: "bedrock",
region: "us-west-2",
auth: "bearer",
models: [],
},
}),
{ fetch },
);
expect(result.text).toBe(ack);
expect(result.receipt).toMatchObject({
inputTokens: 100,
outputTokens: 15,
});
const [url, init] = fetch.mock.calls[0] as unknown as [string, RequestInit];
expect(url).toBe(
"https://bedrock-runtime.us-west-2.amazonaws.com/model/test-model/converse",
);
expect(new Headers(init.headers).get("authorization")).toBe(
"Bearer fixture-key",
);
expect(JSON.parse(String(init.body)).inferenceConfig.maxTokens).toBe(256);
});
it.each(connections)(
"aborts once without inventing a billing outcome for %j",
async (metadata) => {
const controller = new AbortController();
const fetch = vi.fn(async () => {
controller.abort();
throw new DOMException("aborted", "AbortError");
});
const result = await runFastResponseProvider(
{ ...input(metadata), signal: controller.signal },
{ fetch },
);
expect(result).toMatchObject({
errorCode: "timeout",
receipt: { costStatus: "unpriced" },
});
expect(fetch).toHaveBeenCalledTimes(1);
},
);
it.each(connections)(
"never retries provider rejection or retains error bodies for %j",
async (metadata) => {
const fetch = vi.fn(
async () =>
new Response(
JSON.stringify({ error: { message: "SECRET provider input" } }),
{ status: 401, headers: { "content-type": "application/json" } },
),
);
const result = await runFastResponseProvider(input(metadata), { fetch });
expect(fetch).toHaveBeenCalledTimes(1);
expect(result.errorCode).toBe("provider_auth_failed");
expect(result.noProviderWork).toBe(true);
expect(JSON.stringify(result)).not.toContain("SECRET");
},
);
it("rejects long output without discarding its charge", async () => {
const body = chatResponse();
body.choices[0].message.content = "x".repeat(321);
const result = await runFastResponseProvider(
input({ provider: "openrouter", method: "api_key" }),
{ fetch: async () => response(body) },
);
expect(result.text).toBeUndefined();
expect(result.errorCode).toBe("invalid_output");
expect(result.receipt.costStatus).toBe("reported");
});
it("does not invent a price", () => {
expect(fastResponseReceipt({ usage: { inputTokens: 5 } })).toMatchObject({
inputTokens: 5,
costCents: null,
costStatus: "unpriced",
});
});
});
function expectAttribution(url: string, init: RequestInit) {
const headers = new Headers(init.headers);
const expected = new URL(url).hostname === "openrouter.ai";
expect(headers.get("http-referer")).toBe(expected ? "https://paperclip.ing" : null);
expect(headers.get("x-openrouter-title")).toBe(expected ? "Paperclip" : null);
expect(headers.get("x-openrouter-categories")).toBe(expected ? "cloud-agent,programming-app" : null);
}