Skip to content

feat(transcription): transcribe with any OpenAI-compatible server - #546

Open
chhoumann wants to merge 11 commits into
feat/transcription-openrouter-modelsfrom
feat/transcription-openai-compatible
Open

chhoumann wants to merge 11 commits into
feat/transcription-openrouter-modelsfrom
feat/transcription-openai-compatible

Conversation

@chhoumann

@chhoumann chhoumann commented Nov 14, 2026 •

Copy link
Copy Markdown
Owner

Why

Freedom of choice among transcription providers, in a codebase where adding a provider stays cheap and each task has one way to do it. This PR and #544 were built in parallel on #542 and overlapped, so this branch now merges #544 and stacks on it, with one way to read an OpenAI-shaped answer and one json fallback per job.

Summary

Freedom of choice for transcription. A user can now transcribe with any server that speaks OpenAI's /v1/audio/transcriptions API: a local whisper.cpp, faster-whisper or speaches server, so the audio stays on their own machine, or a hosted one such as Groq or Together. They are no longer tied to the four built-in providers. Stacked on #544, which builds on #542.

It follows the recipe that added ElevenLabs and OpenRouter: one CREDENTIALS row, one provider file (providers/openaiCompatible.ts), one registry line, plus the settings this provider needs.

  • Settings. Choosing OpenAI-compatible server shows Transcription provider, Server address (placeholder http://localhost:8000/v1), OpenAI-compatible server API key, Model as a text field, then the shared rows. An address without http:// or https:// says so under the field. Speaker labels behaves as for any model without them: it reads OpenAI-compatible server does not label speakers. and can only be turned off.
  • Address. https://api.groq.com/openai/v1 and .../v1/audio/transcriptions reach the same endpoint, after spaces and trailing slashes are trimmed. Only http and https.
  • Private hosts. requestWithTimeout and assertFetchableUrl take a privateHosts option, and only this provider's sender passes it, because the user typed the address. Feeds, artwork, transcripts, chapters and the hosted providers keep the old policy, which still blocks localhost and private networks.
  • Key and gate. The key is optional. With none selected the request has no Authorization header. A selected key that is not on this device still stops the job. An empty model or a non-http(s) address stops it with a notice that says what to enter.
  • Request. Multipart model, file, response_format=verbose_json, timestamp_granularities[]=word and segment, in parts of 24 MB with no length cap. A 400 whose body names response_format, verbose_json or timestamp_granularities, as OpenAI's gpt-transcribe answers, is sent once more with response_format=json and no timestamp option, and every later part of the job goes straight to json. Any other 400 is not resent.
  • Answer. words[] become timed paragraphs, punctuated from text when the words carry none, else segments[] become timed paragraphs through feat(transcription): offer Voxtral Mini Transcribe 3 and Qwen3 ASR on OpenRouter #544's { segments } transcript, else text becomes untimed paragraphs.
  • Errors stay PodNotes' own sentences: The server at <host> rejected the API key., The server at <host> has no <model> model at that address. for a 404, PodNotes could not reach the server at <host>. for no answer. A refused json resend reads OpenAI-compatible server did not answer in a format PodNotes reads.
  • Privacy. The profile says url: false, so the audio is always uploaded and no episode address, public or private, reaches the server.
  • Panel. The player's Transcript panel reads Transcribe with your server without the Paid tag, because PodNotes cannot tell whether the user's server charges, and Transcribe with OpenAI-compatible server Paid overflowed a 300 px sidebar. The failed-published-transcript notice offers your OpenAI-compatible server without the API credit clause.

Merge with #544

feat/transcription-openrouter-models (#544, 92b5a19) is merged into this branch, and the base is now #544's branch.

  • Conflicts. providers/types.ts keeps feat(transcription): offer Voxtral Mini Transcribe 3 and Qwen3 ASR on OpenRouter #544's Segment type and { segments } PartTranscript variant. The verify skill's transcripts recipe keeps both providers' sections, with the OpenAI notice now naming GPT Transcribe as feat(transcription): offer Voxtral Mini Transcribe 3 and Qwen3 ASR on OpenRouter #544 changed it. The OpenAI-compatible e2e journey is renamed from J-TR-11 to J-TR-13, because feat(transcription): offer Voxtral Mini Transcribe 3 and Qwen3 ASR on OpenRouter #544's Voxtral and Qwen3 ASR journeys took J-TR-11 and J-TR-12.
  • One segments path. The OpenAI-compatible provider returns { segments } for a segments-only answer, which cues.ts times across parts as it does for Voxtral. Its segments-as-words reader is deleted.
  • One verbose_json reader. OpenRouter and OpenAI-compatible servers answer in OpenAI's verbose_json shape, so both now read it through providers/verboseJson.ts: words when the answer lists any, else segments, else nothing. feat(transcription): offer Voxtral Mini Transcribe 3 and Qwen3 ASR on OpenRouter #544's punctuated(words, text) moved there from openrouter.ts, so an OpenAI-compatible server that lists bare words, as a local Qwen or Whisper server can, gets its punctuation from text too. OpenRouter keeps "no speech" for an answer with neither, and the OpenAI-compatible provider keeps text for it.
  • json fallback once per job. OpenAI's API answers verbose_json for gpt-transcribe with 400 "response_format 'verbose_json' is not compatible with model ...". The fallback used to live in the provider, per part, so every part was uploaded twice. It now lives in the job (transcribe.ts). After the first such 400 the job asks for json for that part and every later one, through a textOnly field on PartInput.

Verification

After the merge

All with Node 22.23.2 through fnm exec, on head 3fe2dab:

  • npm run lint, format:check, typecheck, build, size, test (124 files, 2,932 tests), test:tooling (48 tests) and docs:build exit 0.
  • npm run test:e2e -- -t "J-TR" passes 13 tests, J-TR-11 (Voxtral), J-TR-12 (Qwen3 ASR) and J-TR-13 (OpenAI-compatible) included. npm run test:e2e -- tests/e2e/settings-tab.test.ts passes 11.
  • npm run size: the merge commit alone, built from a git archive export, measures 331,775 raw and 108,094 gzip bytes. With the shared reader and the job-level fallback the branch measures 331,542 and 108,060. The budgets are those plus the headroom the stack keeps, 2,554 raw and 97 gzip bytes: 334,096 and 108,157. scripts/size-baseline.md has an entry for the merge, and AGENTS.md names both budgets.
  • The two new tests fail on the merge commit's own source, and pass on the branch.

Plants, each through npm run plant. Every one failed its named test and passed once restored:

Plant Caught by
Fallback per part again (textOnly reset after each part) after a server refuses verbose_json, the episode's next part is sent once, as json
The OpenAI-compatible provider skips the punctuation repair (text withheld from the reader) words a server lists without punctuation take their punctuation from its text
Keep the bare word on a match (#544) Qwen's words, which it lists without punctuation, take their punctuation from its text
One token of lookahead (#544) the same test
OpenRouter saves an answer with neither words nor segments as untimed text (#544) an answer with neither words nor segments writes no note
Part offset dropped for segments (#544) Voxtral's segments from two parts of at most 900 s become paragraphs timed from the episode's start
Ignore segments an answer with segments and no words is saved as its note
Read words under the segment key an answer with words is saved as its note
Ignore json text an answer with text alone is saved as its note
Prefer segments over words sends one multipart request for word and segment times with the key, and times each paragraph
No json fallback (job rethrows the format 400) a server that refuses verbose_json is asked once more for json, and the note is untimed
The fallback asks for verbose_json again the same test
No json fallback (textOnly never set) the same test
Fall back twice (a third send) a server that refuses json too is not asked a third time
Fall back on any 400 a 400 about anything else is not sent again

One branch went instead of getting a test. Planting "fall back twice" as the removal of a textOnly || guard was not caught, because that guard only stopped a resend when a later part's json request was refused too. That is implausible, and it costs one bounded duplicate send, so the guard is deleted.

Runtime, Obsidian 1.14.4 on Linux, this worktree's isolated instance, fresh vault. fixtures.mjs server printed http://podnotes.test:26951/v1. I wrote Fixtures/Big Fixture.mp3 with ffmpeg -f lavfi -i sine=frequency=440:duration=1900 -ac 1 -b:a 128k, 1,900 s and 30,401,140 bytes, so it goes in two parts under the 24 MB cap. In Settings > PodNotes I chose OpenAI-compatible server with the keyboard, typed the address and the model json-only, which the fixture refuses verbose_json for with gpt-transcribe's 400. data.json read provider: "openai-compatible", model: "json-only", serverUrl: "http://podnotes.test:26951/v1". Then I picked Play with PodNotes from the file menu and ran Transcribe current episode from audio from the palette. fixtures.mjs transcriptions showed three requests:

{"authorization":null,"model":"json-only","responseFormat":"verbose_json","timestampGranularities":["word","segment"],"fileType":"audio/mp3","fileBytes":15200800}
{"authorization":null,"model":"json-only","responseFormat":"json","timestampGranularities":[],"fileType":"audio/mp3","fileBytes":15200800}
{"authorization":null,"model":"json-only","responseFormat":"json","timestampGranularities":[],"fileType":"audio/mp3","fileBytes":15200340}

The second part went once, as json, and the two parts sum to the file's bytes. transcripts/local file/Big Fixture.md was saved and opened with both parts' text, untimed. The notice read "Saved without speaker labels, because OpenAI-compatible server does not label speakers. These providers do: ElevenLabs, Deepgram, OpenRouter.", since speaker labels were on as on a new install. dev:errors reported none. The instance and the fixture server are stopped.

Untimed note from a two-part MP3 after one json fallback

Before the merge

  • npm run test passes 2,923 unit tests, 35 new. lint, format:check, typecheck, build, test:tooling (48 passed) and docs:build pass.
  • npm run test:e2e -- tests/e2e/journey-resilience.test.ts -t "J-TR" passes 10 tests, the new J-TR-11 (now J-TR-13) included. The journey fixture server now also answers POST /v1/audio/transcriptions. J-TR-11 seeds the provider with the server's address plus a trailing slash and no key, runs Transcribe current episode, and checks the timed note and the one request: no Authorization, the model, verbose_json, both granularities and the episode's audio byte for byte. The request goes through Obsidian's requestUrl to 127.0.0.1. npm run test:e2e -- tests/e2e/settings-tab.test.ts tests/e2e/journey-transcript.test.ts tests/e2e/focused-settings-save.test.ts passes 27 tests.
  • npm run size: the base built from an export of feat(transcription): one speaker label per voice across the parts of an episode #542's head measures 327,446 raw and 106,873 gzip bytes, this branch 330,739 and 107,768. Both budgets rise by the measured delta, 3,293 raw and 895 gzip, to 333,293 and 107,865, which keeps the base's 2,554 and 97 bytes of headroom. This is the first rise of the raw budget. AGENTS.md now names both.
  • Every plant below failed its named test and passed once restored, through npm run plant, each run against the test's own file (37 of 37):
Plant Caught by
Always send Authorization with no key selected, runs and sends no Authorization header
A selected key missing on this device is ignored a selected key that is not on this device stops the job...
Append the path to a full URL too the address " https://api.groq.com/openai/v1/audio/transcriptions/ " sends to the server's transcriptions endpoint
Trim one trailing slash only the address "https://api.groq.com/openai/v1//" ...
Do not trim spaces the address " https://.../audio/transcriptions/ " ...
Accept any scheme asks for an http(s) address in place of "ftp://files.example.com/v1"
No address check before the job asks for an http(s) address in place of "localhost:8000/v1"
No json fallback (format 400 read as rejected) a server that refuses verbose_json is asked once more for json...
The fallback asks for verbose_json again the same test
Fall back on any 400 a 400 about anything else is not sent again
Fall back twice a server that refuses json too is not asked a third time
Ask for word times only sends one multipart request for word and segment times with the key...
Prefer segments over words the same test
Read words under the segment key an answer with words is saved as its note
Ignore segments an answer with segments and no words is saved as its note
Ignore json text an answer with text alone is saved as its note
Claim speaker labels with speaker labels on, saves the note without them and says so...
No model check Transcribe current episode from audio asks for a model when none is entered...
Model not trimmed Transcribe current episode asks for a model when none is entered...
The sender keeps private hosts blocked a server on this machine or a private network at http://192.168.1.20:9000/v1 is reached
The request boundary drops privateHosts reaches this device or a private network only when asked, under every other rule
The request boundary allows private hosts for every caller the same test
Private hosts skip the other target rules the same test
A rejected key names the provider, not the server a rejected key says what to check, in PodNotes' words
No answer names the provider no answer says what to check...
A 404 names the provider no such address or model says what to check...
The profile takes addresses a public feed's episode is uploaded to a local server, and no request holds its address
The profile takes addresses and the private-feed check is gone a private feed's episode is uploaded to a local server...
The model dropdown shows for a server choosing it shows its address, optional key and model text rows, and saves what is typed
The Server address row is hidden its rows come in order: provider, address, key, model, then the shared rows
No address error an address without http:// or https:// says so under the field
A server's speaker row names the model ID Speaker labels can only be turned off, as for any model without them
Migration drops serverUrl carries an OpenAI-compatible server's address and model to the library that imports it
The same plant keeps a stored provider, model, server address and speaker settings as they are
The offer charges a server the offer names the user's own server, without a charge
The panel names the provider for a server names the user's own server without calling it paid
The panel calls a server paid the same test

Obsidian runtime

Obsidian 1.14.4 on Linux, this worktree's isolated instance, fresh vault. The verify skill's fixture server gains a transcription route, fixtures.mjs server printed http://podnotes.test:34900/v1, which the instance reaches at 127.0.0.1 over the real network (net.mjs logged the POST and did not answer it). The local file is Fixtures/Interview.mp3, 30 s of a 440 Hz tone at 32 kbps, 120,608 bytes, made with ffmpeg.

  1. Settings > PodNotes > Transcripts, Transcription provider chosen with the keyboard. The rows came in the order above. localhost:34900/v1 showed the error under the field. Then http://podnotes.test:34900/v1/ and whisper-large-v3-turbo, typed into the fields. data.json read provider: "openai-compatible", model: "whisper-large-v3-turbo", serverUrl: "http://podnotes.test:34900/v1/", openAICompatibleSecretId: "".
  2. Play with PodNotes from the file menu, then Transcribe current episode from audio from the palette, with no key. transcripts/local file/Interview.md opened with [00:00:00] and [00:00:12] paragraphs. The server recorded one request: {"authorization":null,"model":"whisper-large-v3-turbo","responseFormat":"verbose_json","timestampGranularities":["word","segment"],"fileType":"audio/mp3","fileBytes":120608}. Speaker labels were on, as on a new install, so the notice said the server does not label speakers.
  3. Model json-only, which the fixture refuses verbose_json for with gpt-transcribe's 400: two requests, verbose_json then json with no granularities, and an untimed note.
  4. An empty Model: Enter a model for your OpenAI-compatible server in the transcript settings., no request.
  5. Address http://podnotes.test:1/v1: three POSTs, then Transcription failed: PodNotes could not reach the server at podnotes.test:1. Check the address in the transcript settings.
  6. The player's Transcript panel read Transcribe with your server, and its button transcribed the episode.

dev:errors reported none. The instance and the fixture server are stopped.

Settings with the provider's rows

Server address without a scheme

Timed note from a local MP3

Untimed note after the json fallback

Unreachable server notice

Transcript panel button

Not checked here: real servers. The lead verifies against OpenRouter's and OpenAI's own endpoints with real keys. Pre-existing and not changed: a settings row with an error message narrows its text field, as Download path does.

Real servers after the merge (lead)

Obsidian 1.14.4, palette command, 60 s local clip of E231, a pass-through request log, build of the head after the fix below:

  • OpenAI's API (https://api.openai.com/v1, gpt-transcribe): verbose_json refused with 400, resent once as json, untimed 150-word note.
  • OpenRouter's endpoint given as the full .../audio/transcriptions/ URL (openai/whisper-large-v3-turbo): one verbose_json request, timed note.

The first rerun after the merge saved "only see sci -fi as cultural criticism" and "science -fictional": the merged reader prefers words, and Whisper lists each word with its leading space and splits "sci-fi" into " sci" and "-fi" (captured live). A piece without a leading space now continues the word before it; bare word lists (Grok, Scribe v2, MAI, Qwen) are unchanged. After the fix the real note reads "only see sci-fi as cultural criticism. They try to turn all these science-fictional stories". Plant: skip the join (caught by joins a Whisper word list, whose words carry their own leading space, back into its words). Checks: lint, format, typecheck, size (108,114 of 108,157), 2,933 unit tests, 48 tooling tests.

Release impact

Additive settings, schemaVersion stays 2: transcript.serverUrl (the address as typed) and the root openAICompatibleSecretId (SecretStorage podnotes-openai-compatible-api-key). A settings export carries the address and, when keys are included, secrets.openAICompatible. A device on 2.24.0 keeps both fields on its saves. An unknown provider string is still kept and refused as before. The provider is unreleased, so no released behavior changes. Docs follow after release, as for the other providers.

Note

Add OpenAI-compatible server as a transcription provider

  • Adds an openai-compatible provider that sends multipart audio to a user-entered server endpoint (/audio/transcriptions appended to the configured address), with an optional API key, a user-entered model, and a verbose_json to json fallback when the server rejects timed output (openaiCompatible.ts)
  • Settings UI gains Server address, API key, and text Model rows for this provider, with address validation and disabled speaker labels (PodNotesSettingsTab.ts); transcript settings, migrations, persistence, import/export, and credential registry all support the new fields
  • assertFetchableUrl and requestWithTimeout gain a privateHosts option so local and private-network servers are allowed for this provider only (networkRequest.ts)
  • Extracts shared verbose-JSON parsing (word joining, punctuation restoration, segment parsing) from the OpenRouter provider into verboseJson.ts, with improved handling of Whisper-style split word pieces and punctuation matching over longer gaps
  • Transcript panel and failure notices use "your server" wording and drop the Paid/API-credit tags for this provider; bundle size budgets raised to 334,096 raw and 108,302 gzip bytes
  • Risk: the punctuation-restoration matching strategy in punctuated word helper changed for all verbose-JSON providers (including OpenRouter) — check verboseJson.ts matching behavior against existing OpenRouter transcript output

Macroscope summarized b961dd4.


Note

Medium Risk
Introduces user-controlled transcription endpoints and an opt-in network policy that allows localhost/private hosts for those requests, which expands attack surface if settings are misused but stays scoped to explicit server URLs and upload-only audio.

Overview
Adds openai-compatible as a fifth transcription provider so users can point PodNotes at any server that implements OpenAI’s /v1/audio/transcriptions API (local Whisper stacks or hosted Groq/Together-style endpoints). Settings gain transcript.serverUrl, an optional openAICompatibleSecretId, and UI for server address plus a free-text model; speaker labels stay off for this provider, and the transcript panel shows Transcribe with your server without the Paid tag.

The new provider always uploads episode audio (no URL passthrough), normalizes the user’s base URL to the transcriptions path (keeping query params), and sends multipart verbose_json with word/segment granularities. transcribe.ts retries once per job with plain json when the server rejects timed formats; parsing is shared with OpenRouter via verboseJson.ts (Whisper word joining and punctuation from text). requestWithTimeout / assertFetchableUrl gain an opt-in privateHosts flag used only for this user-entered address.

Persistence, export/import, migrations, fixture/e2e harnesses, and verify docs are extended accordingly; bundle size budgets rise to 334,096 raw and 108,302 gzip.

Reviewed by Cursor Bugbot for commit b961dd4. Bugbot is set up for automated code reviews on this repo. Configure here.

requestWithTimeout and assertFetchableUrl take a privateHosts option. With
it, a target on this device or a private network (localhost, 127.0.0.1,
192.168.x.x, .local and the other non-public ranges) passes the target
policy. Every other rule still applies: http or https only, an explicit
authority, no fragment, no surrounding whitespace. Without the option,
which every existing caller omits, the policy is unchanged, so feeds,
artwork, transcripts and the hosted providers stay blocked from private
targets.
A sixth provider, "OpenAI-compatible server", sends audio to any server that
speaks OpenAI's /v1/audio/transcriptions API: a local whisper.cpp,
faster-whisper or speaches server, Groq, Together, or OpenAI itself. It is a
credential row, one provider file and a registry line, plus two settings.

Settings. transcript.serverUrl holds the address as typed, and
transcript.model the model ID the user types, both additive with
schemaVersion still 2. The key reference is the new root field
openAICompatibleSecretId, stored in SecretStorage as
podnotes-openai-compatible-api-key, and a settings export carries the key as
secrets.openAICompatible. With the provider chosen, the Transcripts rows read
Transcription provider, Server address, OpenAI-compatible server API key,
Model as a text field, then the shared rows. An address that is not http or
https says so under the field.

Address. A base such as https://api.groq.com/openai/v1 and the full
.../audio/transcriptions URL reach the same endpoint, after spaces and
trailing slashes are trimmed. Only this provider's requests may reach this
device or a private network, because the user typed the address.

Gate. The key is optional: with none selected the request carries no
Authorization header. A selected key that is not on this device still stops
the job. An address that is not http or https, or an empty model, stops the
job with a notice that says what to enter.

Request. Multipart with model, file, response_format=verbose_json and word
and segment timestamp granularities, in parts of 24 MB with no length cap.
A 400 that names the response format or the timestamp option, as OpenAI's
gpt-transcribe answers, is sent once more as json without timestamps. Words
become timed paragraphs, segments when there are no words, and text untimed
paragraphs. The provider labels no speakers, and audio is always uploaded,
so no episode address, public or private, reaches the server.

The player's Transcript panel reads "Transcribe with your server", without
the Paid tag, because PodNotes cannot tell whether the user's server
charges. The notice after a published transcript fails offers "your
OpenAI-compatible server" without the API credit clause.

Errors stay PodNotes' own sentences. A 401 says the server at <host>
rejected the API key, a 404 that it has no such model at that address, and
no answer that PodNotes could not reach the server at <host>.
…hine

The journey fixture server also answers POST /v1/audio/transcriptions as a
Whisper server does, with verbose_json words for two sentences 12 s apart,
and records each request. J-TR-11 points the OpenAI-compatible provider at
it with a trailing slash and no key, runs Transcribe current episode, and
checks the timed note and the one request: no Authorization header, the
model, verbose_json, word and segment granularities, and the episode's
audio byte for byte. The request travels Obsidian's requestUrl to
127.0.0.1, so it also proves a server on this machine is reached.
… fixture server

fixtures.mjs server prints http://podnotes.test:<port>/v1, a route on the
fixture server that answers POST /v1/audio/transcriptions with verbose_json
words, or json's text when asked for json, and refuses verbose_json for the
model json-only with the 400 OpenAI's gpt-transcribe gives.
fixtures.mjs transcriptions prints each request it answered. Runtime checks
reach it over Obsidian's real network path with no paid key. The
transcripts recipe covers the provider's settings, a local file, the json
fallback and the refusals.
…ovider measures

The branch this builds on leaves main.js at 327,446 raw and 106,873 gzip
bytes, built with the same vite command from an export of its head. The
OpenAI-compatible server provider, its settings rows, its gate and failure
sentences and the panel's server wording add 3,293 raw and 895 gzip bytes,
which takes main.js to 330,739 and 107,768, over the 330,000 and 106,970
budgets. Each budget rises by its delta, to 333,293 and 107,865, so the 2,554
raw and 97 gzip bytes of headroom stay as they were. This is the first rise
of the raw budget. AGENTS.md now names both budgets.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: b961dd4
Status: ✅  Deploy successful!
Preview URL: https://01866513.podnotes.pages.dev
Branch Preview URL: https://feat-transcription-openai-co.podnotes.pages.dev

View logs

…nce per job

OpenRouter and OpenAI-compatible servers answer in OpenAI's verbose_json
shape, so both now read it through providers/verboseJson.ts: words when the
answer lists any, punctuated from its text when they carry none, else
segments as the { segments } PartTranscript that cues.ts times across parts.
The OpenAI-compatible provider no longer stores segments as words.

A server that refuses verbose_json, as OpenAI's API does for gpt-transcribe,
is asked for json for that part and every later part of the job, so each
later part is uploaded once instead of twice.
@chhoumann
chhoumann changed the base branch from feat/transcription-speaker-merge to feat/transcription-openrouter-models November 14, 2026 12:04
Whisper lists each word with its leading space and splits some words, so
"sci-fi" arrives as " sci" and "-fi". Reading words as separate cues saved
"sci -fi" in the note. A piece without a leading space now continues the
word before it. Lists of bare words, as OpenRouter's other models send,
are unchanged.
…erge from it

Words that a server lists without punctuation take it from the answer's
`text`. The matcher looked only three tokens past the last match and never
moved on a miss, so a stretch of three or more `text` tokens the words leave
out, such as filler, left every later word in the part unpunctuated.

A word still matches a token within three of the last match. A token further
on now counts when the next word follows it in `text` too, so the words find
their place again after a stretch of any length. Each key's positions are
consumed in order, which keeps the pass linear in the words and tokens.

The gzip budget rises by the 125 bytes this measures.
…tions path

The OpenAI-compatible provider appended `/audio/transcriptions` to the whole
address string. An address that carries a query, such as
`http://host/v1?api_key=...`, sent to `/v1?api_key=.../audio/transcriptions`,
which corrupted the token and missed the route. A full endpoint with a query
got the path a second time.

The address is now parsed as a URL. Its path loses trailing slashes and gains
`/audio/transcriptions` only when it lacks it, and the query stays as typed.

The gzip budget rises by the 20 bytes this measures.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant