Skip to content

test(models): LangChain.js e2e acceptance for the /v1 gateway + embeddings encoding_format (#631)#1856

Open
heskew wants to merge 4 commits into
feat/models-v1-gatewayfrom
test/models-v1-langchain-e2e
Open

test(models): LangChain.js e2e acceptance for the /v1 gateway + embeddings encoding_format (#631)#1856
heskew wants to merge 4 commits into
feat/models-v1-gatewayfrom
test/models-v1-langchain-e2e

Conversation

@heskew

@heskew heskew commented Jul 18, 2026

Copy link
Copy Markdown
Member

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:

  • ChatOpenAI non-streaming chat (invoke), asserting content and usage_metadata flow-through
  • ChatOpenAI streaming chat (stream), multiple chunks
  • OpenAIEmbeddings embedQuery + embedDocuments

All three authenticate with a real minted operation token via a shared mintOperationToken helper (extracted from the SDK streaming test's inline version).

Gateway fix: encoding_format on /v1/embeddings

The embeddings leg exposed a real drop-in-compatibility gap: the OpenAI Node.js SDK — which @langchain/openai wraps — defaults to encoding_format: 'base64' when the caller doesn't specify one, and unconditionally base64-decodes the response (openai@6.45.0, resources/embeddings.jstoFloat32Array). 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.5 added as a devDependency — used only by the e2e acceptance test, mirroring the existing openai devDep from #1616 (runtime deps unchanged; dependencies.md untouched per convention).

Verification

  • unitTests/resources/models/v1/*: 39 passing locally
  • Full client matrix verified locally against an isolated built Harper (fresh install boot AND restart boot): raw fetch (base64 round-trip decodes to the fixture's vector), OpenAI SDK streaming chat + embeddings, LangChain ChatOpenAI invoke/stream, OpenAIEmbeddings single + batch — all pass
  • oxlint + prettier clean on changed files

🤖 Generated with Claude Code

https://claude.ai/code/session_01RXx78W5LxbvxEdSyrB1vvn

Docs

encoding_format is 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_schema nesting, developer role) were out of scope here and are tracked in #1857.

…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
@socket-security

socket-security Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​langchain/​openai@​1.5.510010010097100

View full report

@socket-security

socket-security Bot commented Jul 18, 2026

Copy link
Copy Markdown

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.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm js-tiktoken is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/@langchain/openai@1.5.5npm/js-tiktoken@1.0.21

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/js-tiktoken@1.0.21. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm js-tiktoken is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/@langchain/openai@1.5.5npm/js-tiktoken@1.0.21

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/js-tiktoken@1.0.21. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread integrationTests/server/v1-gateway.test.ts Outdated
Comment thread unitTests/resources/models/v1/translation.test.js Outdated
Comment thread unitTests/resources/models/v1/translation.test.js Outdated
Comment thread package-lock.json
Comment thread unitTests/resources/models/v1/translation.test.js Outdated
Comment thread integrationTests/server/v1-gateway.test.ts Outdated
@heskew
heskew requested a review from kriszyp July 18, 2026 01:09
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
Comment thread unitTests/resources/models/v1/translation.test.js Outdated
Comment thread unitTests/resources/models/v1/translation.test.js Outdated
Comment thread integrationTests/server/v1-gateway.test.ts
@claude

claude Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found. The previously-flagged pooled-Buffer/Float32Array alignment blocker is fixed in b66d27c (copies into a fresh Uint8Array before viewing as Float32Array at all three sites).

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
@heskew
heskew marked this pull request as ready for review July 18, 2026 08:27
Comment thread integrationTests/server/v1-gateway.test.ts
… (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 kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 root modelsGateway scope before application directories load. At this point REST.httpOptions is still {}, the listener is registered under the name modelsGateway, and started = true prevents a later application rest scope from applying registration-time options. For example, rest: { webSocket: false } is ignored, and middleware using before: 'rest' / after: 'rest' cannot order against this listener; urlPath/host restrictions are likewise not used for the shared dispatcher. Could this use an HTTP-only /v1 activation path, or defer shared REST startup until the effective REST options are known while retaining the stable rest middleware 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_choice is declared as supported but is never consumed, so this always passes the tools to the backend. With the valid OpenAI request tool_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 for none. If required and named selection cannot yet be represented internally, returning a clear 400 is safer than silently treating them as auto; please add mapper/handler coverage for these cases.
  • resources/models/v1/embeddings.ts:49: Passing undefined here makes every successful embeddings response report prompt_tokens: 0 and total_tokens: 0, even though backends (including this PR's fixture) return usage.embeddingTokens and Models.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?

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.

2 participants