From d78dcb56cfb88ff6833b5ef7bb24d54babba8dd5 Mon Sep 17 00:00:00 2001 From: Dylan Trotter Date: Sat, 26 Sep 2026 23:28:40 +0000 Subject: [PATCH] Accept a full chat-completions URL as the base URL Configuring Fireworks with the URL from its docs produced a 404: Path not found: /v1/chat/completions/chat/completions Providers document the full endpoint, so that is what gets pasted into a setting labelled "API base URL", and the code appended the path a second time. Accept either spelling, say so in the label, and give the skill a worked Fireworks example. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/thread-briefs/server.ts | 2 +- .../skills/thread-briefs/SKILL.md | 14 +++++++++- plugins/thread-briefs/summarize.test.ts | 26 +++++++++++++++++++ plugins/thread-briefs/summarize.ts | 16 ++++++++++-- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/plugins/thread-briefs/server.ts b/plugins/thread-briefs/server.ts index 2150ad7..01fbff5 100644 --- a/plugins/thread-briefs/server.ts +++ b/plugins/thread-briefs/server.ts @@ -38,7 +38,7 @@ export default async function plugin(bb: BbPluginApi) { const settings = bb.settings.define({ baseUrl: { type: "string", - label: "API base URL", + label: "API base URL (root or full /chat/completions endpoint)", default: "https://api.openai.com/v1", }, apiKey: { type: "string", label: "API key", secret: true }, diff --git a/plugins/thread-briefs/skills/thread-briefs/SKILL.md b/plugins/thread-briefs/skills/thread-briefs/SKILL.md index b94d8e5..79cf746 100644 --- a/plugins/thread-briefs/skills/thread-briefs/SKILL.md +++ b/plugins/thread-briefs/skills/thread-briefs/SKILL.md @@ -16,7 +16,7 @@ Set these with `bb plugin config thread-briefs set `. | Key | Default | What it does | | --- | --- | --- | -| `baseUrl` | `https://api.openai.com/v1` | OpenAI-compatible API root. `/chat/completions` is appended; a trailing slash is fine. | +| `baseUrl` | `https://api.openai.com/v1` | OpenAI-compatible endpoint. Either the API root or the full `/chat/completions` URL works; trailing slashes are fine. | | `apiKey` | _(unset, secret)_ | Bearer token for that endpoint. The plugin reports `needs-configuration` until it is set. | | `model` | `gpt-4o-mini` | Model used for summarizing. Any small instruction-following model works. | | `jsonMode` | `true` | Send `response_format: {type: "json_object"}`. Turn **off** for endpoints that reject it (many local servers do). | @@ -25,6 +25,18 @@ Set these with `bb plugin config thread-briefs set `. The key is a secret setting, so it stays on the server and is never sent to the frontend. +Worked example, Fireworks: + +```sh +bb plugin config thread-briefs set baseUrl "https://api.fireworks.ai/inference/v1" +bb plugin config thread-briefs set model "accounts/fireworks/models/glm-5p3-flash" +bb plugin config thread-briefs set apiKey "" +``` + +A 404 naming a doubled path (`/chat/completions/chat/completions`) meant an older +build appended the path to a `baseUrl` that already ended in it. Both spellings +are accepted now. + ## When a brief is regenerated 1. `thread.idle` fires at every turn boundary and starts a `quietSeconds` diff --git a/plugins/thread-briefs/summarize.test.ts b/plugins/thread-briefs/summarize.test.ts index a8f9c31..8809429 100644 --- a/plugins/thread-briefs/summarize.test.ts +++ b/plugins/thread-briefs/summarize.test.ts @@ -77,6 +77,12 @@ describe("parseSummary", () => { }); describe("chatCompletionsUrl", () => { + it("appends the path to an API root", () => { + expect(chatCompletionsUrl("https://api.openai.com/v1")).toBe( + "https://api.openai.com/v1/chat/completions", + ); + }); + it("tolerates a trailing slash", () => { expect(chatCompletionsUrl("https://api.openai.com/v1/")).toBe( "https://api.openai.com/v1/chat/completions", @@ -88,6 +94,26 @@ describe("chatCompletionsUrl", () => { "https://x.test/v1/chat/completions", ); }); + + // Providers document the full endpoint, so it gets pasted in as the base URL. + // Appending blindly produced /chat/completions/chat/completions and a 404. + it("leaves a full Fireworks endpoint alone", () => { + expect( + chatCompletionsUrl("https://api.fireworks.ai/inference/v1/chat/completions"), + ).toBe("https://api.fireworks.ai/inference/v1/chat/completions"); + }); + + it("accepts the Fireworks API root too", () => { + expect(chatCompletionsUrl("https://api.fireworks.ai/inference/v1")).toBe( + "https://api.fireworks.ai/inference/v1/chat/completions", + ); + }); + + it("leaves a full endpoint with a trailing slash alone", () => { + expect(chatCompletionsUrl("https://x.test/v1/chat/completions/")).toBe( + "https://x.test/v1/chat/completions", + ); + }); }); describe("buildUserPrompt", () => { diff --git a/plugins/thread-briefs/summarize.ts b/plugins/thread-briefs/summarize.ts index 86b37f6..1dd65d9 100644 --- a/plugins/thread-briefs/summarize.ts +++ b/plugins/thread-briefs/summarize.ts @@ -131,9 +131,21 @@ export interface CompletionConfig { jsonMode: boolean; } -/** Trailing slashes are the single most common way a base URL is mistyped. */ +/** + * Accept either the API root or the full chat-completions endpoint. + * + * Providers document the full URL — Fireworks publishes + * `https://api.fireworks.ai/inference/v1/chat/completions`, OpenAI the same + * shape — so pasting that into a setting labelled "base URL" is the natural + * mistake, and appending blindly produced + * `/v1/chat/completions/chat/completions` and a 404. Trailing slashes are the + * other common way to mistype it. + */ export function chatCompletionsUrl(baseUrl: string): string { - return `${baseUrl.trim().replace(/\/+$/u, "")}/chat/completions`; + const trimmed = baseUrl.trim().replace(/\/+$/u, ""); + return /\/chat\/completions$/u.test(trimmed) + ? trimmed + : `${trimmed}/chat/completions`; } export async function requestSummary(