Skip to content

Commit f8d4552

Browse files
committed
fix: fail fast on incomplete embeddings response
Addresses PR review (Codex P2). The OpenAI embeddings parser sized the result by len(data) and indexed by the API's index field, so a response with fewer items than inputs — or a duplicate index — could silently misalign embeddings with their inputs or leave an empty [] vector, corrupting downstream pgvector writes/queries. Size the output by len(req.Inputs) and reject out-of-range, duplicate, or missing indexes. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnKgCRfMvFcZAkoVbr8k79
1 parent 07a6e7c commit f8d4552

2 files changed

Lines changed: 39 additions & 7 deletions

File tree

packages/engine/internal/connector/embed_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,30 @@ func TestEmbeddingConnector_ModelAllowlist(t *testing.T) {
120120
}
121121
}
122122

123+
func TestEmbeddingConnector_IncompleteResponseFailsFast(t *testing.T) {
124+
// Server returns only one embedding for two inputs — must error rather
125+
// than silently returning a misaligned/short vectors list.
126+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
127+
w.Header().Set("Content-Type", "application/json")
128+
_ = json.NewEncoder(w).Encode(map[string]any{
129+
"model": "text-embedding-3-small",
130+
"data": []map[string]any{{"index": 0, "embedding": []float64{0.1, 0.2}}},
131+
"usage": map[string]any{"prompt_tokens": 3, "total_tokens": 3},
132+
})
133+
}))
134+
defer server.Close()
135+
136+
c := &EmbeddingConnector{Client: server.Client()}
137+
_, err := c.Execute(context.Background(), map[string]any{
138+
"model": "text-embedding-3-small",
139+
"input": []any{"a", "b"},
140+
"base_url": server.URL,
141+
})
142+
if err == nil {
143+
t.Error("expected error when the provider returns fewer embeddings than inputs")
144+
}
145+
}
146+
123147
func TestEmbeddingConnector_BedrockUnsupported(t *testing.T) {
124148
c := &EmbeddingConnector{}
125149
_, err := c.Execute(context.Background(), map[string]any{

packages/engine/internal/connector/provider_openai.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,18 +225,26 @@ func (p *OpenAIProvider) Embeddings(ctx context.Context, req *EmbeddingRequest)
225225
if err := json.Unmarshal(body, &apiResp); err != nil {
226226
return nil, fmt.Errorf("openai: parsing embeddings response: %w", err)
227227
}
228-
if len(apiResp.Data) == 0 {
229-
return nil, fmt.Errorf("openai: no embeddings returned")
230-
}
231-
232228
// Reassemble in request order — the API tags each item with its index.
233-
out := make([][]float64, len(apiResp.Data))
229+
// Size by the request so a short or duplicate-indexed response fails fast
230+
// rather than silently misaligning embeddings with their inputs.
231+
out := make([][]float64, len(req.Inputs))
232+
seen := make([]bool, len(req.Inputs))
234233
for _, d := range apiResp.Data {
235-
if d.Index < 0 || d.Index >= len(out) {
236-
return nil, fmt.Errorf("openai: embedding index %d out of range", d.Index)
234+
if d.Index < 0 || d.Index >= len(req.Inputs) {
235+
return nil, fmt.Errorf("openai: embedding index %d out of range for %d inputs", d.Index, len(req.Inputs))
236+
}
237+
if seen[d.Index] {
238+
return nil, fmt.Errorf("openai: duplicate embedding index %d", d.Index)
237239
}
240+
seen[d.Index] = true
238241
out[d.Index] = d.Embedding
239242
}
243+
for i, ok := range seen {
244+
if !ok {
245+
return nil, fmt.Errorf("openai: missing embedding for input %d (got %d of %d)", i, len(apiResp.Data), len(req.Inputs))
246+
}
247+
}
240248

241249
return &EmbeddingResponse{
242250
Embeddings: out,

0 commit comments

Comments
 (0)