Fix/pagi 125 release review findings - #1
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WithExtraBody parked its fields inside the shared metadata map, and WithMetadata assigns that map rather than merging into it. Any call that set metadata after the extra body therefore lost the extra body entirely: the openai door sent none of it, and the doors that report the loss stayed silent too, because by then there was nothing left to read. The fields now live in their own CallOptions field, so neither option can reach the other. Both orders are pinned, on the door that merges them and on a door that only reports them. PAGI-293 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The disable path already knew the Google families that never think and sent them nothing; the enable path did not, so gemma-3-27b-it and gemini-2.0-flash received a thinkingConfig for a control their generation has no field for, and the caller's request to think vanished without a word. They now get no thinking config in any mode, and the lost option is reported. The disable-floor warning also fired for gemma-4, telling the operator the disable had only been approximated. For that family the lowest level IS the off position — which is why the door picks it and why the disable is offered at all — so the warning claimed a loss that never happened. PAGI-292 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wire assertions searched the request body for a substring, and a JSON number has no right delimiter: the row asserting a budget of 512 passed just as happily on 5120, so widening any documented floor left the suite green. They now read the parsed thinking config and compare values. The adaptive rows never named an effort, so the guard that separates "you decide" from "think this hard" was pinned by nothing; a row for each generation now holds it, and the range rows cover the ceiling as well as the floor. PAGI-295 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…default Adaptive with no named effort means the caller handed the depth decision to the model. The shared resolver turned that into the top effort, so on doors whose vendor documents no adaptive mode the request went out at the most expensive setting — the opposite of what was asked. On gpt-5.4 and newer carrying tools it went further and refused the call before the network, because the vendor serves an effort with tools on another API only. Such a request now behaves as an unset mode: no effort field, the vendor's own default, and the vendor's own tools rule. Where that means the model will not reason at all — an opt-in generation, or one that demands an explicit disable alongside tools — the caller is told. An adaptive request that does name an effort still sends it, and the doors that have a real adaptive wire are untouched. PAGI-296 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ult (PAGI-296) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-133) Carries the warnings surface and the eleven wire defects found while filling it: PAGI-283, 284, 285, 286, 288, 289, 292, 293, 294, 295, 296. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hint asked ResolveOff with ProviderUnknown for every ollama model, so a name its own vendor serves as mandatory-thinking — deepseek-r1 — came back as OffUnsupported and the caller was told thinking cannot be turned off. The door meanwhile sends think:false, which Ollama documents as accepted for models that think; gpt-oss is the documented exception, where booleans are ignored and the trace cannot be disabled. ProviderOllama now resolves to OffDisableThinkBool everywhere except that family, and the door reads the same resolver instead of its own predicate, so hint and wire cannot classify a model differently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The batcher appended whatever came back, so an answer with fewer vectors than inputs left every later document paired with another document's vector, and an empty answer reached the caller as a short result instead of an error. CheckEmbeddings now gates every batch and gives the doors a single place to ask the same question (PAGI-306). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EmbedQuery indexed the first element of an empty list, so a 200 with no vectors crashed the caller through the Embedder interface; EmbedDocuments accepted a short list and shifted the pairing of texts to vectors (PAGI-306). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cohere leg returns whatever the model listed, so an empty list panicked EmbedQuery and a short list left EmbedDocuments pairing texts with the wrong vectors. Both now report the shared embeddings errors (PAGI-306). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both doors returned the short slice next to ErrUnexpectedResponseLength, so a caller that reads the slice when err is non-nil indexed vectors that belong to other inputs. Neighbouring doors return nothing on that path (PAGI-306). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d partials (PAGI-306) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nothing else The chat route built max_tokens out of WithMaxLength, so WithMaxTokens never reached the wire while the warning surface still claimed the door had no field for it. Value-typed request fields also made an explicit temperature or seed of zero indistinguishable from "not set", and the door dropped them; the request now carries pointers, so zero is sent and an unset option stays off the wire. An empty message list or a non-text first part panicked through llms.Model and now returns a typed error (PAGI-311). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…at it cannot send (PAGI-311) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gConfig The predicate listed nova-2-sonic beside nova-2-lite, so both Bedrock doors built a reasoning config for it while the name catalogue answered that the model does not think. AWS serves Nova 2 Sonic only on InvokeModelWithBidirectionalStream — its model card marks Converse and Invoke unsupported and names no reasoning configuration at all (PAGI-307). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both OpenAI branches left RejectsSampling false while the wire pins temperature to one and drops top_p, so a consumer drawing its controls from the hint showed a live slider for a value the door throws away (PAGI-307). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An "ft:"-wrapped identifier kept the wrapper through name normalisation, so the rules that refuse top_k and repetition_penalty for the base model let them through for its fine-tune and the vendor answered 400 (PAGI-307). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both doors parsed the model's arguments with a plain unmarshal, so an integer wider than float64 came back changed on the way to the next turn: an id or a sum of money arrives as a different number. The idiom bedrock already carried moves to internal/toolcall, which also keeps the key order ollama's ordered map expects, and all three doors now share it (PAGI-307). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…vendor (PAGI-307) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both doors checked truncation only on the success path, so a consumer error or a broken stream swallowed the signal: a caller who asked to fail on truncation got the partial text and an error about something else, while the stop reason already said the output limit was reached. The partial path now joins the truncation error to the one that interrupted the stream (PAGI-310). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fake door and the conformance mock returned nothing next to the error, so tests written against them proved a contract the real doors do not have: every door hands back what it collected. Both now answer with the text delivered so far, buffered before the callback exactly as the real doors do (PAGI-310). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he contract (PAGI-310) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DelegatesDepth read the adaptive flag without looking at the mode, so a config carrying both — reachable by any caller filling the exported struct rather than using the call options, which each replace it whole — reported that the caller had left the depth to the vendor. Doors then took the default branch and the request travelled without the disable wire, which is the opposite of what the caller asked for (PAGI-308). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…a number Two defects on the same door. A thinking budget was refused before the network whenever tools rode along, though the rule the vendor publishes is about reasoning_effort: the budget now travels and only the effort stays off the wire. And the upstream cost reached GenerationInfo as *float64, so a consumer whose type switch knows float64 silently lost it; it now travels as a value, and a cost the vendor did not send leaves no key behind (PAGI-308). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…308) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Introduced `MetadataIndex` struct to define indexes over keys in the `cmetadata` column, including support for exclusion conditions. - Added validation for metadata indexes to ensure keys and names are valid identifiers. - Enhanced `filterPredicates` function to inline keys and return errors for invalid filter keys. - Updated tests to cover new functionality, including metadata index creation and filtering behavior. - Modified existing tests to ensure compatibility with the new filtering logic.
…tion (PAGI-405) init returned on any error without ending its transaction, so on a shared pgxpool every failure kept one connection acquired for the life of the pool, and once all of them were kept New blocked in Ping. The rollback ignores the caller's cancellation: pgx kills the connection when a rollback fails, which would take down a single pgx.Conn passed by WithConn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…re the wire xAI documents that reasoning on grok-4.7 cannot be disabled and answers reasoning_effort "none" with 400. The model now resolves to OffUnsupported: the openai door returns ErrReasoningOffUnsupported, and CannotDisable keeps pentagi from offering Off for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e pinned Capability lookups normalise the name before matching, so the family test lists bare names; route prefixes, case and the Bedrock dotted form belong in TestEverySpellingResolvesToOneEntry, which had no grok group. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ability checks - Added support for "gpt-6-astra", "gpt-6-sol", and "gpt-6-luna" in the reasoning model tests and capability lookups. - Updated the reasoning model identification logic to include the new generations. - Enhanced tests to ensure proper handling of the new model names and their capabilities.
splitPlatformPrefix reads only letters, so the hyphen in us-gov left the region on the name: us-gov.xai.grok-4.7 answered OffOmit where us.xai.grok-4.7 answers OffUnsupported, and the Bedrock door sent a disable request that the vendor refuses. The Bedrock region prefixes now come off in modelSpellings too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hout effort Both refuse function tools on chat completions unless reasoning_effort is none, and the refusal comes even when the field is absent, since the card's default is medium. They now follow the gpt-5.6 rule: an explicit effort with tools fails before the wire, and the default path sends none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Their cards list max, but the vendor's own door answers 400 to it and names none, low, medium, high and xhigh, as it does for gpt-6-astra. The catalogue already follows the endpoint; the test keeps a fix by the card from reaching the wire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e the connection pgx closes the connection when a commit fails outside the idle state, and a commit on a done context fails before it is sent. A caller that cancelled after the last init step lost the single pgx.Conn it passed by WithConn, and the shielded rollback never ran. init now returns the context error first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan test built its own WITH ... AS MATERIALIZED text, so hoisting the filter out of the fence in the store kept it green. It now records the query the store runs and explains that with the same arguments; the hoisted filter turns it red. The index name comes from indexName instead of a literal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The name joined keys with the separator they may contain and marked any exclusion with a bare "partial", so different declarations shared a name and CREATE INDEX IF NOT EXISTS skipped all but the first. Every derived name now ends in a hash of the table, the keys and the excluded pairs; two declarations that still share a name with different DDL fail with ErrInvalidMetadataIndex. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ard folds case The readable prefix is cut at 54 bytes, which could split a multi-byte table name and make CREATE INDEX fail on an invalid byte sequence; the cut now drops the partial rune. PostgreSQL folds unquoted names to lower case, so two explicit names that differ only in case are one index and the guard now refuses them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An identical index under another name on the shared table, such as one built under the old derived name, won the plan and failed the test although a metadata index carried the scan. The test now declares its index on tables of its own and drops them afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mpling gpt-5.6 and gpt-6-sol/luna get reasoning_effort none when function tools ride without an effort, yet the door still counted thinking as running and dropped temperature, top_p and the penalties. The vendor accepts all of them at none and refuses them only at an active effort. None on the wire now means no thinking. PAGI-220 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not their SQL Comparing rendered statements carried the name's own case into the comparison, so two identical declarations whose names differed only in case were refused, though PostgreSQL builds them as one index. The guard now compares the canonical definition that the derived name already hashes: table, keys and exclusions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne name A guard comparing keys alone passed every case before this one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ls rule, sampling under a tools-forced none (PAGI-125) Derived metadata index names carry a hash of the whole declaration, a cancelled init no longer loses a single pgx.Conn, the plan test explains the live statement, gpt-6-sol and gpt-6-luna take function tools only at none, and that none keeps the caller's sampling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l and adjust test options for output budget. Enhance llmtest to support call options, allowing tests to specify required parameters for model interactions.
…mplement refreshIndex function for Opensearch tests to ensure document visibility during searches
…g checks and implementing centralized credential validation for OpenAI API across multiple test files
…RoundTrip and implement TestRecordLeavesTheTransportItsOwnBody to ensure body integrity during transport
- Added `WithCloudStructuredOutputFallback()` option to allow schema-constrained output for Ollama Cloud, which ignores the format field. - Updated `GenerateContent` to inject schema instructions into user messages when using the fallback. - Enhanced validation logic to ensure responses conform to the provided schema, with local validation for cloud responses. - Introduced tests for structured output fallback scenarios, including both streaming and non-streaming cases. - Updated documentation to reflect changes in handling structured outputs for cloud models.
….ai models - Introduced `WithStructuredOutputFallback()` option to enable schema-constrained output for models that do not support json_schema. - Updated `GenerateContent` to handle schema instructions in user messages, ensuring compatibility with DeepSeek and MiniMax APIs. - Enhanced validation logic for responses to conform to provided schemas, with local validation for cloud responses. - Added comprehensive tests for structured output fallback scenarios, including both streaming and non-streaming cases. - Refactored existing tests to accommodate new structured output handling.
sirozha
left a comment
There was a problem hiding this comment.
Automated code review at max effort. This was a single-pass review done without the Agent tool: no multi-agent fan-out and no separate verification pass. I worked through every review angle myself and re-checked each finding against the diff. Several findings were confirmed with throwaway probes against the PR head (355dc71). Those probes are marked "checked against the PR tree" in the comments, and none were committed.
There are 15 inline comments, ranked most severe first: 11 correctness issues, 1 efficiency issue and 3 cleanups.
Several of the findings share one root cause. Vendor-API rules are keyed only on the model name and ignore the host: TakesNoJSONSchema, RejectsPenalties and the Qwen stream-only rule. ServedByDeepSeek(model, host) and DashScopeRoute(model, host) already show the host-aware pattern that would fix all three.
Three findings depend on vendor behaviour that I could not re-measure here: the Gemini penalty 400, the Vertex maxOutputTokens range, and Gemini omitting args on zero-argument calls. Please weigh those accordingly.
Generated by Claude Code
| if takesOnlyGPTOSSLevels(model) { | ||
| return &api.ThinkValue{Value: gptOSSLevel(effort)} | ||
| } | ||
| if level := (&api.ThinkValue{Value: string(effort)}); level.IsValid() { |
There was a problem hiding this comment.
Regression: think is now sent to models that cannot think. resolveThink puts think ("high"/"medium"/"low"/true) on every ReasoningOn request without checking the model's capability. Ollama's /api/chat rejects a truthy think for a model without the thinking capability (server/routes.go in the pinned v0.32.5: "%q does not support thinking", HTTP 400). The base door never set think, so the same call used to succeed.
Scenario: ollama.New(ollama.WithModel("llama3.2")) + GenerateContent(..., llms.WithReasoning(llms.ReasoningHigh, 0)) → the request carries "think":"high" (checked via createChatRequest) → 400. The same happens for gemma3, mistral and other non-thinking models.
Fix: send think only for models known or likely to think (for example reasoning.LikelyReasoningModel, or the capabilities from /api/show). For other models, omit it and add a WarningDrop.
Generated by Claude Code
| func filterPredicates(prefix string, filter map[string]any, argOffset int) ([]string, []any, error) { | ||
| keys := make([]string, 0, len(filter)) | ||
| for k := range filter { | ||
| if !metadataKeyPattern.MatchString(k) { |
There was a problem hiding this comment.
Regression: filter keys that are not bare identifiers now fail. filterPredicates rejects any key that does not match ^[A-Za-z_][A-Za-z0-9_]{0,62}$. Filters that worked before now return ErrInvalidFilterKey: keys containing -, ., :, spaces or non-ASCII characters, and keys longer than 63 bytes.
Scenario: store.SimilaritySearch(ctx, q, 5, vectorstores.WithFilters(map[string]any{"doc-id": "42"})) → filter key must be a bare identifier: "doc-id". The base produced (cmetadata ->> 'doc-id') = '42' and returned rows.
Keeping the index reachable only requires the key to be inlined as a literal, and quoteLiteral already renders one safely. Rendering cmetadata ->> <quoteLiteral(k)> here and in ddl keeps both the injection fix and the index match, without narrowing which keys are accepted.
Generated by Claude Code
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if delim, ok := open.(json.Delim); !ok || delim != '{' { |
There was a problem hiding this comment.
Regression: null arguments are now an error. DecodeFields/Decode reject arguments that are the JSON literal null (ErrNotAnObject). The json.Unmarshal calls they replaced in the googleai and bedrock doors accepted null as an empty map.
The googleai door produces exactly that string itself. json.Marshal(part.FunctionCall.Args) yields "null" when Gemini returns a call without args, as it can for a parameterless function. That case is more likely now that convertTools declares such functions without parameters.
Scenario: on turn 1, Gemini calls a zero-argument tool, so ToolCall.Arguments == "null". On turn 2, replaying that AI message goes through convertParts → toolcall.Decode("null") → toolcall: arguments are not a JSON object: null, and the whole GenerateContent fails. The Bedrock door fails the same way.
Fix: treat a null (or empty) document as an empty object before requiring {.
Generated by Claude Code
| results := make([]anthropicclient.Content, 0, len(msg.Parts)) | ||
| for _, part := range msg.Parts { | ||
| toolCallResponse, ok := part.(llms.ToolCallResponse) | ||
| if !ok { |
There was a problem hiding this comment.
Tool messages with any extra part now fail. The loop returns ErrInvalidContentType for any part that is not a ToolCallResponse. The base used Parts[0] and ignored the rest, and the OpenAI door's handleToolMessage explicitly tolerates TextContent in tool messages. A tool message such as [ToolCallResponse{...}, TextContent{Text: ""}], which worked before, now aborts the request with anthropic: invalid content type for tool message.
Fix: skip TextContent parts here (or append non-empty ones as a text block after the tool_result blocks), and return an error only when no ToolCallResponse is present.
Generated by Claude Code
| // An expression index carries no statistics of its own until the table is | ||
| // analysed, and until then the planner costs it off the column's own | ||
| // distribution and can reject it outright. | ||
| if _, err := tx.Exec(ctx, "ANALYZE "+s.embeddingTableName); err != nil { |
There was a problem hiding this comment.
Efficiency: ANALYZE runs on every store start. createMetadataIndexesIfNotExist issues ANALYZE <table> on every New() that declares metadata indexes, not only after an index was created. That contradicts the comment above, which says "every start afterwards is a catalog lookup". The ANALYZE runs inside the init transaction while it holds the embedding-table advisory lock. Each store construction therefore samples the embeddings table, and concurrent constructions queue behind it.
Fix: check to_regclass(<index name>) IS NULL before each CREATE INDEX, and run ANALYZE only when at least one index was actually built. Autovacuum keeps the statistics fresh after that.
Generated by Claude Code
| // | ||
| // HasExplicitTokens reports whether the caller set a token budget of its own, | ||
| // rather than leaving the budget to be derived from an effort. | ||
| func (r *ReasoningConfig) HasExplicitTokens() bool { |
There was a problem hiding this comment.
Detached doc comment: HasExplicitTokens and DelegatesDepth were inserted between GetEffort's doc comment and func GetEffort. Godoc now shows the whole GetEffort returns enum value… paragraph as the documentation of HasExplicitTokens, and GetEffort has no documentation. Move the two new methods, with their own comments, below GetEffort.
Generated by Claude Code
sirozha
left a comment
There was a problem hiding this comment.
Second release review of this PR: multi-agent, ultra depth, on 355dc71.
How it was run. The net diff da7016e..355dc71 (the previous release is exactly the merge base) was split into 14 slices:
- 8 source slices: reasoning tables, openai, anthropic, bedrock, googleai/vertex, mistral/huggingface/ollama, core
llms, vectorstores/chains/embeddings. Each was reviewed twice: a first pass, then an independent pass hunting for what the first one missed. - 4 test-quality slices.
- Test infrastructure and cassettes.
- Cross-cutting checks: public API diff against the base (
apidiff),go vet,golangci-lint,go test -race,go mod, examples and docs.
Every finding was then checked by two independent verifiers. One tried to reproduce it with probe tests on HEAD and on the base (through go test -overlay, no files changed); the other tried to refute it. A judge settled disagreements. 117 findings were raised and 106 survived; after merging duplicates across slices, that gives the 87 inline comments below (3 high, 46 medium, 34 low, 4 nit). "pre-existing" marks defects the base already had in lines this PR touches. Findings already posted in the earlier review were not repeated.
Main themes
- Model-name rules that ignore the host. Rules measured on a vendor's own API also fire on OpenRouter, vLLM, Groq and Together:
effort_wire.golines 29, 49, 76, 180, 211 and 301,off.go171 and 195,claude_capability.go:49. This is the same root cause as the earlier comments onstructured_output.go:235,openaillm.go:311andeffort_wire.go:12.ServedByDeepSeek(model, host)andDashScopeRoute(model, host)already show the host-aware shape. - googleai/vertex on the GenAI SDK breaks existing Vertex callers. Credentials plus an API key now fail, and the error prints the key.
WithEndpointinhost:portform no longer works, the default location is gone,ClientOptionsare dropped in favour of ADC,WithHTTPClientsilently loses auth, and exported identifiers were removed. - Tests that never run in CI or cannot fail. The new pgvector integration tests and the Ollama Cloud cassette tests always skip, and several assertions cannot fail (see the test comments).
Worth listing in the release notes (API and behaviour breaks)
vertexexported errors and constants removed.perplexity.ModelSonarReasoningandModelR11776removed.reasoning.ClaudeSupportsEffortWithBudgetgained aProviderparameter. This is intentional (d07f16d), so there is no inline comment.llmtest.MockLLM.GenerateContentStreamremoved (intentional, f9ca9d5).reasoning.OffEffortNone/OffUnsupportedrenumbered (iota).- Bedrock
GenerationInfocounters moved fromint32toint, but only partly (seeprovider_ai21.go:360). WithGRPCConn/WithGRPCClientare now refused.ValidateReasoningnow refuses"none".
Re-check of the earlier review. 12 of its 15 comments were confirmed. Three were not:
llms/googleai/option.go:52(16384 default output tokens): the change is deliberate (476f3a6), and the base also always sent a default. The 8k-output models in the scenario (gemini-2.0-flash/flash-lite) have been shut down.llms/ollama/ollamallm.go:358(duplicated gpt-oss mapping): the claimed divergence cannot happen.gptOSSLevel("")is unreachable, becauseGetEffortnever returns""on the ReasoningOn path, and for every reachable effort both copies agree.embeddings/jina/options.go:86(comment vs BatchSize): the comment is accurate, and the dimension-derived default is unchanged from the base. The real problem nearby is theWithBatchSize(0)panic on line 87.
Those three threads can be resolved.
Checks that passed and limits.
go vet ./...andgolangci-lintv2.12.2 (the CI version) are clean on HEAD.go test -racefound no data races.go mod tidy -diffis clean and all 75 examples build.- Every changed cassette parses and replays, no cassette is orphaned, and the changed cassettes carry only placeholder credentials.
- There were no live vendor calls (no API keys). AWS, OpenRouter, Pinecone and Perplexity docs were unreachable from the sandbox, so comments that depend on vendor behaviour say so.
- Tests that need Docker did not run. The pgvector findings were checked on a local PostgreSQL 16 + pgvector 0.6.
Generated by Claude Code
| embs, err = FetchAmazonTextEmbeddings(ctx, b.client, b.ModelID, batch) | ||
| case "cohere": | ||
| embeddings, err = FetchCohereTextEmbeddings(ctx, b.client, b.ModelID, batch, CohereInputTypeText) | ||
| embs, err = FetchCohereTextEmbeddings(ctx, b.client, b.ModelID, batch, CohereInputTypeText) |
There was a problem hiding this comment.
[Medium · pre-existing] Cohere batches on Bedrock use the default batch size 512, above the vendor's 96-texts-per-call limit.
Pre-existing, not a regression, but this loop was edited in this PR. With default options, Cohere batches use BatchSize 512, and each batch goes to Bedrock as a single texts array. AWS documents a maximum of 96 texts per call for Cohere Embed on Bedrock. A probe that used WithClient against an httptest endpoint showed NewBedrock(WithModel(ModelCohereEn)).EmbedDocuments(ctx, 200 texts) sending one InvokeModel with 200 texts, the same at HEAD and at base. Against real Bedrock (not tested live) that should return a ValidationException, and no documents get embedded. Suggest capping the batch size at 96 for cohere.* models, e.g. min(b.BatchSize, 96) or a Cohere-specific default, and noting the limit on WithBatchSize.
Generated by Claude Code
| if _, ok := _models[o.Model]; ok { | ||
| o.BatchSize = _models[o.Model] | ||
| // _models holds vector dimensions, not batch sizes. | ||
| if size, ok := _models[o.Model]; ok && !o.batchSizeFromCaller { |
There was a problem hiding this comment.
[Medium] jina WithBatchSize(0) or a negative value now panics.
Regression: once WithBatchSize is called, applyOptions keeps the caller's value even when it is 0 or negative, and EmbedDocuments then passes it to embeddings.BatchTexts, which divides by it. With NewJina(WithBatchSize(0)), typical for an unset config field, EmbedDocuments(ctx, []string{"a","b"}) panics with integer divide by zero, and WithBatchSize(-1) panics with makeslice: cap out of range. On the previous release both calls embedded the two texts with BatchSize=512. I showed this with an httptest-backed probe run against both HEAD and BASE. Suggested fix: honour the caller only for positive values (o.batchSizeFromCaller && o.BatchSize > 0), or fall back to a sane default before batching.
Generated by Claude Code
| for _, k := range keys { | ||
| predicates = append(predicates, fmt.Sprintf("(%scmetadata ->> '%s') = $%d", | ||
| prefix, k, argOffset+len(args)+1)) | ||
| args = append(args, fmt.Sprintf("%v", filter[k])) |
There was a problem hiding this comment.
[Low · pre-existing] Float filter values in exponent form never match stored JSON numbers.
Non-string filter values are bound as fmt.Sprintf("%v", v). For float64, %v switches to exponent form (from 1e21 up and below 1e-4), while the json column holds what encoding/json wrote, and ->> returns that text. Every number in a filter decoded from a JSON request body is a float64. So {"user_id": 1234567} binds "1.234567e+06" against a stored 1234567, and {"ts": 1727500000} binds "1.7275e+09". SimilaritySearch then returns 0 documents with no error, while the same filter with an int matches. I confirmed the mismatch with a probe that compares filterPredicates' bound value with json.Marshal of the same value. The previous release never matched non-string values either, so this is an incomplete fix rather than a regression, but commit ec0ee19 advertises that such filters now match. Suggested fix: render numbers the way encoding/json does, e.g. strconv.FormatFloat(v, 'f', -1, 64) for floats, or json.Marshal for any non-string scalar.
Generated by Claude Code
| } | ||
| } | ||
|
|
||
| const extraBodyUnread = "the door builds its request through a vendor SDK and has nowhere to merge them" |
There was a problem hiding this comment.
[Nit] ExtraBody drop warning names a vendor SDK the Anthropic door does not use.
The anthropic door's reason for dropping WithExtraBody says "the door builds its request through a vendor SDK and has nowhere to merge them". That is not true for this door: internal/anthropicclient marshals its own messagePayload with encoding/json, and llms/anthropic imports no vendor SDK (unlike ollama, which uses ollama/api). The string looks copied from the SDK-based doors. So WithExtraBody(map[string]any{"metadata": ...}) on anthropic returns a drop warning that blames an SDK that isn't used, and suggests the field can't be supported when the door could actually merge it into the body it builds. Suggest either giving anthropic its own accurate reason (for example "the door does not merge extra body fields yet") or actually merging ExtraBody into the marshalled payload, for both the messages path (line 15) and the completions path (line 172).
Generated by Claude Code
| }, nil | ||
| } | ||
|
|
||
| // converseToolChoice carries the caller's choice to the wire. An unset or |
There was a problem hiding this comment.
[Nit] Two doc comments sit on the wrong functions after the refactor.
This doc comment names converseToolChoice but is attached to carriesToolBlocks, which does something else: it scans the history for toolUse/toolResult blocks, and line 177 uses it to keep the tool config even when the choice is "none". converseToolChoice (line 774) is left with no comment. The same thing happened at line 838: // processStreamingResponse processes streaming events now documents the newly inserted deliverToolCall, and processStreamingResponse (line 867) has none. You can see both by reading HEAD lines 760-783 and 838-867; new functions were inserted between each comment and the function it names. Fix: move each comment back above its function and give carriesToolBlocks and deliverToolCall their own comments (for example, say that tool blocks in the history require a toolConfig on Bedrock, which is why "none" falls back to auto).
Generated by Claude Code
| @@ -69,7 +71,7 @@ func TestReasoningEffortPassthroughToWire(t *testing.T) { | |||
|
|
|||
| // WithAdaptiveReasoning with no explicit effort must not silently disable | |||
| // reasoning here: it defaults to high, matching the Anthropic/Bedrock paths. | |||
| func TestAdaptiveReasoningNoEffortDefaultsToHighOnWire(t *testing.T) { | |||
| func TestAdaptiveReasoningWithNoEffortSendsNoEffort(t *testing.T) { | |||
There was a problem hiding this comment.
[Nit] Stale comment states the opposite of the renamed test's assertion.
The comment above this test is stale: it still says adaptive reasoning with no effort "defaults to high, matching the Anthropic/Bedrock paths". The renamed test now asserts the opposite, failing if any reasoning_effort is sent, which is the behaviour a459111 introduced (the vendor picks its default). The test passes at 355dc71, so the comment, not the assertion, is wrong. A maintainer who trusts the comment could restore reasoning_effort: high for delegated requests, bringing back the most expensive effort that a459111 removed. Suggest rewording it to something like: "WithAdaptiveReasoning with no explicit effort hands the depth decision to the model: no reasoning_effort is sent, so the vendor default applies."
Generated by Claude Code
| // Verbosity asks the model for a shorter or longer answer. | ||
| Verbosity *string `json:"verbosity,omitempty"` | ||
| // InferenceSpeed picks the inference configuration. Not to be confused with | ||
| // Speed above, which is the voice rate of speech synthesis. |
There was a problem hiding this comment.
[Nit] InferenceSpeed doc points to Speed "above", but Speed is declared below.
Nit: this comment says "Not to be confused with Speed above", but the TTS Speed field is declared further down in CallOptions (line 348, under // TTS options.). Please change "above" to "below", or just name the field ("the TTS Speed field").
Generated by Claude Code
Adds the multi-agent review posted on PR #1, the re-check of the first review's 15 comments, and the steps left for the next session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kz4afhEXUCzEsB3PYRmo6n
Saves what the two reviews of PR #1 produced beyond the PR comments: every finding with both verifiers' verdicts, the rejected ones, the agents' unpublished notes, the apidiff list and the probe tests, plus a P0/P1/P2 triage grouped by fix area. CLAUDE.md now lists what has to be decided before the fixes start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kz4afhEXUCzEsB3PYRmo6n
Release: the wire each vendor documents, across every door
fix/PAGI-125-release-review-findings→main-vxcontrol. 421 commits sincev0.1.14-update.7, 418 files, +26102 / −2455. Follow the previous releases and cut thepaired
release/…branch from the merge commit.What this is
Two streams of work. The first closed the review of
v0.1.14-update.5..update.7. The secondcame out of measuring what actually reaches each vendor: for every door, every model family
and every reasoning-related parameter, what we send was compared against what the vendor
documents, and the differences were fixed. Fifty-two issue keys are referenced across the
range; the composition lives in the two checklists of PAGI-125.
The user-visible shape of it: reasoning is expressed in each vendor's own mechanism instead of
one shape for everyone, sampling is no longer taken away from models whose vendor accepts it,
the output limit travels under the field name each vendor knows, and requests that cannot
work fail locally with a typed error instead of a round trip to a 400.
The compatibility decision — read this first
This release does not preserve full behavioural backward compatibility, and that is
deliberate. Supporting current model generations across every provider was chosen over
keeping the wire byte-identical.
A wire change was accepted only when one of two things held: the old request was rejected by
the vendor anyway, or the change gives the caller back something that used to be dropped
silently. A change that takes away working behaviour is a defect, not a compatibility
trade-off — three such cases were found and two were fixed (the third is a documented
decision about disabling Claude thinking behind an OpenAI-compatible transport).
That criterion was checked, not assumed. A deterministic harness (
httptestin place of thevendor, normalised request bodies, 286 model/option combinations) was diffed against
v0.1.14-update.7: 95 combinations differ, none of them a loss — 34 are the base's owntemperature: 1that the caller never asked for, 38 were rejected by the vendor anyway, 12never reached the vendor, 9 restore something to the caller, and 2 turn a vendor 400 into a
local error.
Breaking changes
WithReasoningDisabled()returns a typed error wherever the disable cannot reach the wire,instead of sending nothing or letting the provider answer 400.
tools/perplexitydropsModelSonarReasoningandModelR11776for models the vendorretired. Code referencing them will not compile.
reasoning.ClaudeSupportsEffortWithBudgettakes a provider:(model string, p Provider) bool.ContentChoicegainedTruncated;reasoning.ContentReasoninggainedRedacted. Keyedliterals are unaffected; unkeyed ones no longer compile.
vertexreports the vendor wire spelling inStopReason.The full list, with the reasoning behind each, is in the release notes prepared for the tag.
Where the risk is, and how to review it
The diff is large but it is not uniform. Almost all of the behavioural risk sits in two places:
llms/reasoning— the tables and predicates that decide, per model name, whichmechanism reaches the wire. Everything else consults them. Start here.
llms/openai,llms/anthropic,llms/bedrock,llms/googleai,llms/mistral,llms/ollama. Each door translates the same intent into its own wire form,so the same decision can be right in one and wrong in another; the tests are written per
door for that reason.
The rest is tests, cassettes and documentation. A useful reading order is the fifty contract
decisions recorded in PAGI-125: each says what the library now guarantees and why, and each
maps to a predicate you can grep for.
Verification
go build ./...,go vet ./...,gofmt -l .,go mod tidy -diffandgolangci-lint runare clean; all 75 examples build via
make build-examples.go test ./llms/...— 24 packages green.eighteen models, both the current and previous generation of each vendor. Zero panics, zero
invariant violations, no error caused by the library.
families and the effort levels behind the gateway — and the cassettes were re-recorded from
those runs, so the recorded traffic matches what the library now sends.
three changes, all listed above. The scan predates the last Bedrock work, which adds one
more exported name,
ContentReasoning.Redacted, also listed above.Running the tests locally: set placeholder AWS credentials
(
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION) beforego test ./llms/bedrock/....Without them two client-construction tests fail instead of skipping — the httprr guard skips
only when both the cassette and the credentials are missing, and the cassettes are committed.
This is a pre-existing wart, filed for the follow-up patch.
Known red outside
llms/:chainsandchains/constitutionneed network, and thevectorstores/*packages need their databases running. They are red onv0.1.14-update.7aswell; compare against that baseline rather than against zero.
Not in this PR
The remaining library debt is deliberately left out and collected for the next patch: a
conformance suite for the Bedrock door that exercises the Converse path pentagi actually uses,
the effort-to-budget estimate for high, tool-call argument re-serialisation on the anthropic
door, and three items that never reach a consumer. Each was re-verified against this tree
before being deferred.
Release mechanics
The tag name is not chosen yet and the choice matters: every
v0.1.14-*release so far is aprerelease that
go get -udoes not select, whilev0.1.15would leave prerelease space andbecome the
go get -utarget for everyone pinned tov0.1.14. Both consumers are currentlyon
v0.1.14-update.7.