test(models): LangChain.js e2e acceptance for the /v1 gateway + embeddings encoding_format (#631)#1856
test(models): LangChain.js e2e acceptance for the /v1 gateway + embeddings encoding_format (#631)#1856heskew wants to merge 4 commits into
Conversation
…encoding_format (#631) Closes the LangChain.js leg of #631's acceptance criterion ("unmodified LangChain.js / OpenAI SDK client completes a chat"): ChatOpenAI invoke, ChatOpenAI streaming, and OpenAIEmbeddings (single + batched) against a real Harper instance, authenticating with a minted operation token via the shared mintOperationToken helper (extracted from the SDK streaming test). The embeddings leg exposed a drop-in-compatibility gap: the OpenAI Node.js SDK (which LangChain wraps) defaults to encoding_format: 'base64' when the caller doesn't specify one and unconditionally base64-decodes the response — a float-array response comes back silently corrupted, not rejected. The gateway now honors encoding_format ('float' | 'base64', 400 on anything else) and returns base64-encoded little-endian float32 bytes when asked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXx78W5LxbvxEdSyrB1vvn
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request adds support for the base64 encoding format in the /v1/embeddings endpoint to ensure compatibility with the OpenAI Node.js SDK and LangChain.js. It also introduces end-to-end integration tests for LangChain.js and adds corresponding unit tests. The review feedback highlights a potential alignment issue in several tests where Float32Array is instantiated directly from a pooled Buffer.buffer, which can throw a RangeError if the byte offset is not a multiple of 4. The reviewer suggests copying the buffer bytes into a new Uint8Array to guarantee proper alignment.
The original lock regeneration (npm 11) pruned optional-peer subtrees the branch lockfile still records, producing ~700 lines of unrelated churn. npm 10 (what CI's Node 22 uses) yields a purely additive delta: only the @langchain/openai tree is added, zero entries removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXx78W5LxbvxEdSyrB1vvn
|
Reviewed; no blockers found. The previously-flagged pooled- |
Buffer.from(str, 'base64') can return a pooled buffer whose byteOffset is not guaranteed 4-byte-aligned; viewing it directly as Float32Array can throw RangeError. Copy into a fresh buffer at the three decode sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXx78W5LxbvxEdSyrB1vvn
… (review) Per @kriszyp: the acceptance test targets silent base64 corruption, but a regression returning the JSON float array while the SDK requested base64 decodes to [0] — a nonempty finite array that passed the shape-only assertions. Assert the fixture's exact dimension (4) and values ([0.1,0.2,0.3,0.4]) for both embedQuery and each embedDocuments vector so the full unmodified LangChain/OpenAI-SDK base64 round trip is validated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXx78W5LxbvxEdSyrB1vvn
kriszyp
left a comment
There was a problem hiding this comment.
I realize this is a tests PR, but the codex comments do raise an interesting point. Maybe this is better situated as its own middleware that then calls the http function (or some extraction of it), maybe providing custom resources instead of putting this into the standard resources map/registry?
Proposed inline comments (anchors failed):
resources/models/v1/index.ts:43: This starts the process-wide REST/WS singleton from the rootmodelsGatewayscope before application directories load. At this pointREST.httpOptionsis still{}, the listener is registered under the namemodelsGateway, andstarted = trueprevents a later applicationrestscope from applying registration-time options. For example,rest: { webSocket: false }is ignored, and middleware usingbefore: 'rest'/after: 'rest'cannot order against this listener;urlPath/hostrestrictions are likewise not used for the shared dispatcher. Could this use an HTTP-only/v1activation path, or defer shared REST startup until the effective REST options are known while retaining the stablerestmiddleware name? A boot-order test combining the enabled gateway with application REST options would pin this down.resources/models/v1/chatCompletions.ts:47:req.tool_choiceis declared as supported but is never consumed, so this always passes the tools to the backend. With the valid OpenAI requesttool_choice: 'none', a tools-capable backend may still return a tool call; a non-tools backend can fail capability selection instead of generating text. At minimum, please omit tools fornone. Ifrequiredand named selection cannot yet be represented internally, returning a clear 400 is safer than silently treating them asauto; please add mapper/handler coverage for these cases.resources/models/v1/embeddings.ts:49: Passingundefinedhere makes every successful embeddings response reportprompt_tokens: 0andtotal_tokens: 0, even though backends (including this PR's fixture) returnusage.embeddingTokensandModels.embed()records it internally. That gives OpenAI-compatible clients false accounting data. Could the gateway-facing model call preserve the backend usage and pass it through, with an end-to-end assertion for the fixture's nonzero count?
Follow-on to #1616 (stacked on
feat/models-v1-gateway; retargets to main automatically if this merges after it).What
Closes the LangChain.js leg of #631's acceptance criterion — "End-to-end test: an unmodified LangChain.js or OpenAI SDK client successfully completes a chat against a Harper instance". #1616 already covers the OpenAI-SDK half; this adds the LangChain.js half to the same integration suite:
ChatOpenAInon-streaming chat (invoke), asserting content andusage_metadataflow-throughChatOpenAIstreaming chat (stream), multiple chunksOpenAIEmbeddingsembedQuery+embedDocumentsAll three authenticate with a real minted operation token via a shared
mintOperationTokenhelper (extracted from the SDK streaming test's inline version).Gateway fix:
encoding_formaton/v1/embeddingsThe embeddings leg exposed a real drop-in-compatibility gap: the OpenAI Node.js SDK — which
@langchain/openaiwraps — defaults toencoding_format: 'base64'when the caller doesn't specify one, and unconditionally base64-decodes the response (openai@6.45.0,resources/embeddings.js→toFloat32Array). A float-array response isn't rejected; it comes back as silently corrupted vectors. The gateway previously ignored the param entirely.Now:
encoding_format: 'float' | 'base64'honored (base64 = little-endian float32 bytes, matching the OpenAI wire format), anything else → OpenAI-shape 400. Unit tests cover round-trip, subarray views (no buffer over-read), handler validation; integration tests cover the raw-fetch base64 round-trip and the reject path, and the LangChain/OpenAI-SDK embeddings tests exercise the base64 default end-to-end.devDependency
@langchain/openai@^1.5.5added as a devDependency — used only by the e2e acceptance test, mirroring the existingopenaidevDep from #1616 (runtime deps unchanged;dependencies.mduntouched per convention).Verification
unitTests/resources/models/v1/*: 39 passing locallyoxlint+prettierclean on changed files🤖 Generated with Claude Code
https://claude.ai/code/session_01RXx78W5LxbvxEdSyrB1vvn
Docs
encoding_formatis user-facing API surface, but docs for the whole/v1/*gateway ride with #1616's docs decision — no separate docs PR here; the base64 support should be folded into whatever gateway page #1616 produces.Cross-model review (HEG step 10)
Standard mode (Gemini leg + Harper-domain adjudication): no blockers or significant concerns in the diff. The lockfile churn it flagged has been fixed (regenerated with npm 10 for an additive-only delta). Findings on pre-existing gateway code (400-vs-500 envelope consistency,
json_schemanesting,developerrole) were out of scope here and are tracked in #1857.