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
19 changes: 19 additions & 0 deletions src/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,25 @@ function parseScores(value: unknown, candidates: readonly string[]): {
found.set(entry.token, entry.logprob);
}

const sampled = content[0];
if (typeof sampled.token === "string" && expected.has(sampled.token)) {
if (typeof sampled.logprob !== "number" || !Number.isFinite(sampled.logprob)
|| sampled.logprob > 0 || sampled.logprob <= CLAMPED_LOGPROB) {
throw new ScoringError(`OpenRouter returned an invalid or clamped logprob for label ${sampled.token}.`);
}
if (sampled.bytes !== undefined && sampled.bytes !== null) {
if (!Array.isArray(sampled.bytes) || sampled.bytes.length !== 1
|| sampled.bytes[0] !== sampled.token.charCodeAt(0)) {
throw new ScoringError(`OpenRouter returned invalid bytes for label ${sampled.token}.`);
}
}
const topLogprob = found.get(sampled.token);
if (topLogprob !== undefined && topLogprob !== sampled.logprob) {
throw new ScoringError(`OpenRouter returned conflicting logprobs for label ${sampled.token}.`);
}
found.set(sampled.token, sampled.logprob);
}

if (found.size === 0) {
throw new ScoringError("OpenRouter did not return logprobs for any choice label.");
}
Expand Down
27 changes: 24 additions & 3 deletions tests/openrouter.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const unorderedTopLogprobs = Object.freeze([

function scoredPosition(topLogprobs = unorderedTopLogprobs) {
return {
token: "A", bytes: [65], logprob: -1.2338635921, top_logprobs: topLogprobs,
token: "A", bytes: [65],
logprob: topLogprobs.find(({ token }) => token === "A")?.logprob ?? -1.2338635921,
top_logprobs: topLogprobs,
};
}

Expand Down Expand Up @@ -64,7 +66,7 @@ function chooser(f, extra = {}) {
});
}

test("uses first-position label logprobs instead of the sampled token", async () => {
test("uses first-position label logprobs", async () => {
const value = response();
value.usage = {
prompt_tokens: 180,
Expand Down Expand Up @@ -158,14 +160,33 @@ test("assigns zero probability to labels omitted from top logprobs", async () =>
.reduce((sum, probability) => sum + probability, 0) - 1) < 1e-12);
});

test("includes the sampled label when it is absent from top logprobs", async () => {
const top = unorderedTopLogprobs.filter(({ token }) => token !== "C");
const value = response(top, {
logprobs: { content: [{ token: "C", bytes: [67], logprob: -1.1088635921, top_logprobs: top }] },
});
const decision = await chooser(fixture(value))(request);

assert.equal(decision.choice, "billing");
assert.equal(decision.scores.billing, -1.1088635921);
assert.ok(decision.distribution.billing > 0);
});

const malformed = [
["null logprobs", response(undefined, { logprobs: null }), /logprobs/i],
["no scored position", response(undefined, { logprobs: { content: [] } }), /exactly one/i],
["multiple scored positions", response(undefined, {
logprobs: { content: [scoredPosition(), scoredPosition()] },
}), /exactly one/i],
["no choice labels", response([{ token: "x", bytes: [120], logprob: -0.1 }]),
["no choice labels", response([{ token: "x", bytes: [120], logprob: -0.1 }], {
logprobs: { content: [{ token: "x", bytes: [120], logprob: -0.1,
top_logprobs: [{ token: "x", bytes: [120], logprob: -0.1 }] }] },
}),
/any choice label/i],
["conflicting sampled logprob", response(unorderedTopLogprobs, {
logprobs: { content: [{ token: "A", bytes: [65], logprob: -1.5,
top_logprobs: unorderedTopLogprobs }] },
}), /conflicting.*A|A.*conflicting/i],
["duplicate required label", response([...unorderedTopLogprobs, unorderedTopLogprobs[0]]), /duplicate.*A|A.*duplicate/i],
["invalid logprob", response(unorderedTopLogprobs.map((entry) =>
entry.token === "C" ? { ...entry, logprob: 0.1 } : entry)), /logprob/i],
Expand Down
Loading