Skip to content

Phase 1 Wave 1 — LLMProvider seam types (1.A) + ToolNormalizer (1.E) + CostTracker (1.B)#7

Merged
cemililik merged 9 commits into
mainfrom
development
Jun 6, 2026
Merged

Phase 1 Wave 1 — LLMProvider seam types (1.A) + ToolNormalizer (1.E) + CostTracker (1.B)#7
cemililik merged 9 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

First Wave-1 batch of Phase 1: the pure-TS seam trio in @relavium/llm, all built on the
frozen seam — no provider SDK (those land with the adapters in 1.C/1.G/1.H). Plus the roadmap
marker flipping 1.L.0 to ✅ done.

Commits

What's here

1.A — the immovable seam (packages/llm/src/types.ts): LlmRequest / LlmMessage / ToolDef
/ Usage / CapabilityFlags / LlmError / LlmResult / StreamChunk as Zod schemas, LlmProvider
as a TS interface. ContentPart + AbortSignalLike live in @relavium/shared (the dependency
direction requires it) and are re-exported by the seam. @types/json-schema is a dev/type-only dep.

1.E — ToolNormalizer: one canonical ToolDef → the three native wire shapes (no vendor type);
the Gemini OpenAPI-subset reshape (keeps anyOf + numeric/string bounds, strips the rest, throws
a typed ToolSchemaError on a $ref); synthesized + FIFO-matched Gemini tool-call ids.

1.B — CostTracker + pricing: pricing.ts (the canonical table, integer micro-cents/MTok — the
DB unit; Anthropic confirmed via claude-api, others VERIFY-marked); cost() (each token class billed
once, inputTokens net of cache), CostTracker (per-attempt accumulator → cost:updated figures);
typed UnknownModelError (never a silent zero).

Conformance

  • TypeScript-first, strict; no any / unsafe as
  • No vendor SDK type crosses the seam — fence airtight (these files are NOT adapters)
  • No new runtime dependency (@types/json-schema is dev/type-only)
  • No secret in any error message; cost is Relavium's, never read from a provider field
  • Canonical-home docs consistent (llm-provider-seam.md §6 inputTokens-net clarified)
  • pnpm turbo run lint typecheck test build green · +59 tests in @relavium/llm (39) + @relavium/shared content (+6) · format clean · seam fence airtight · no DB drift

Verification

Each workstream passed an independent adversarial multi-agent review. The cost dimension was clean;
the review caught and we fixed: the Gemini reshape over-stripping anyOf/bounds, an empty-id gap in
normalizeToolCall, and a seam-doc↔code inputTokens convention divergence (§6 now clarified).

Tracked follow-ups (deferred-tasks.md)

  • Verify the non-Anthropic placeholder prices when each adapter lands (1.G/1.H).
  • Add a cache_write column to model_catalog (or knowingly drop it) when the catalog seeder lands.

Refs: ADR-0011

🤖 Generated with Claude Code

Summary by Sourcery

Introduce the frozen provider-agnostic LLMProvider seam in @relavium/llm, including normalized messaging and streaming types, tool normalization, and cost tracking with canonical model pricing, while updating roadmap docs to mark Phase 1 as in progress.

New Features:

  • Add the @relavium/llm package exposing the LLMProvider seam types, request/response schemas, and streaming chunk union built on shared content primitives.
  • Provide a ToolNormalizer that maps canonical ToolDef definitions to provider-specific wire formats and back into canonical tool_call content parts, including Gemini-specific schema reshaping and id synthesis.
  • Introduce a CostTracker with a canonical micro-cent model pricing table for Anthropic, OpenAI, Gemini, and DeepSeek models, and expose typed config errors for unknown models and unsupported tool schemas.

Enhancements:

  • Reconcile shared content primitives into @relavium/shared and re-export them from the LLM seam to keep provider-agnostic contracts centralized.
  • Clarify llm-provider seam documentation around input token accounting vs cache tokens and how adapters must normalize provider usage.
  • Update Phase 1 roadmap and deferred tasks to reflect Wave 0 completion, Wave 1 activation, and follow-up work on pricing verification and database schema alignment.

Summary by CodeRabbit

  • New Features

    • Added a provider-agnostic LLM package: sealed provider seam, exported seam types/APIs, canonical model pricing, integer micro-cent cost tracking, tool normalization (Gemini schema reshaping, stable tool-call ids), and typed seam errors.
  • Documentation

    • Clarified token billing semantics (input/read/write) across providers; updated Phase 1 roadmap, waves, and deferred tasks; added package README and status notes.
  • Tests

    • Added unit and invariants tests for pricing/cost-tracking, tool normalization, seam types, and shared content contracts.

cemililik and others added 3 commits June 5, 2026 16:29
Phase 1 Wave 0 — the @relavium/shared reconciliation (1.L.0) — is merged. Flip the phase
status to In progress, mark the 1.L.0 workstream ✅ Done, and rewrite current.md's immediate
next steps to the two Wave-1 lanes per the sequencing plan: 1.A (freeze the LLMProvider seam
types, scaffold packages/llm) and 1.L (WorkflowYAMLParser, scaffold packages/core).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase-1 Wave-1 workstream 1.A — scaffold @relavium/llm and freeze the immovable
LLMProvider seam in src/types.ts, expressed only in Relavium/Zod types
(ADR-0011, llm-provider-seam.md). No provider SDK yet — adapters land in 1.C/1.G/1.H.

@relavium/llm (new package, mirrors @relavium/shared: tsc build, catalog deps, types:[]
platform-free):
- Zod schemas (+ inferred types): LlmRequest, LlmRole/LlmMessage, ToolDef, ToolChoice,
  Usage, CapabilityFlags, LlmErrorKind/LlmError, LlmResult, StreamChunk (the 6-variant
  discriminated union). LlmProvider is a TS interface (it carries methods: id +
  generate(req,key) + stream(req,key) + supports).
- ToolDef.parameters is typed JSONSchema7 (z.custom; @types/json-schema is a dev/type-only
  catalog dep — erased at build, no ADR) — the deep subset reshape is the ToolNormalizer's
  job (1.E).
- Curated index.ts (not export *) re-exporting the seam + the shared substrate.
- 19 tests: per-schema accept/reject + type-level pins proving the public types are pure
  Relavium types (ProviderId set, StopReason set, a stub LlmProvider compiles) — a leaked
  vendor type would fail them, and the import-zone fence forbids the import outright.

@relavium/shared — the seam substrate that the dependency direction (shared → llm) requires
to live in shared so the future session schemas can reuse it without a circular import:
- content.ts: ContentPartSchema (text | tool_call | tool_result) and AbortSignalLike (the
  minimal structural cancellation handle — lib:["ES2023"] has no AbortSignal, and a real one
  satisfies it structurally, so the platform-free packages need no DOM lib / @types/node).
- +6 tests (173 total). The seam re-exports ContentPart/StopReason from here.

Full gate green (12/12 turbo); seam fence airtight (packages/llm/src/types.ts is fenced —
only src/adapters/* may import a vendor SDK); db drift gate unchanged.

Refs: ADR-0011

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two pure-TS Wave-1 workstreams on the Relavium side of the seam (no provider SDK; the fence
forbids vendor imports outside src/adapters/*). Both build on the frozen 1.A seam types.

1.B — CostTracker + pricing:
- pricing.ts: the canonical MODEL_PRICING table keyed on canonical model id — the in-code source
  of truth the adapters and the tracker share, seeded later into the (empty) model_catalog.
  Prices are integer micro-cents per million tokens (USD x 1e8) — the DB unit, no float. Anthropic
  rows confirmed via claude-api (Opus 4.8 $5/$25, Sonnet 4.6 $3/$15, Haiku 4.5 $1/$5, + cache
  read/write); OpenAI/Gemini/DeepSeek are best-known placeholders (VERIFY at 1.G/1.H, tracked).
- cost-tracker.ts: priceModel (unknown id -> typed UnknownModelError, never a silent zero),
  cost(modelId, usage) = round(tokens x ratePerMtok / 1e6) per token class billed once
  (inputTokens net of cache reads), and a CostTracker accumulator producing the cost:updated
  figures across per-attempt usage.
- errors.ts: typed UnknownModelError + ToolSchemaError (discriminated `code`).

1.E — ToolNormalizer:
- toWire(toolDef, providerId): one canonical ToolDef -> the three native wire shapes (OpenAI/
  DeepSeek function, Anthropic input_schema, Gemini functionDeclarations) as Relavium-defined
  plain objects — no vendor type.
- reshapeForGemini: the OpenAPI-subset reshape — allow-lists the keywords Gemini honors (incl.
  anyOf and the numeric/string bounds), strips the rest, drops unsupported `format` values, and
  THROWS a typed ToolSchemaError on a `$ref` it cannot express.
- GeminiToolCallIds + normalizeToolCall: synthesize stable tool-call ids (Gemini has none) and
  match functionResponses FIFO per name; fold an extracted {id,name,args} into a canonical
  tool_call ContentPart, rejecting an empty id/name from a misbehaving provider.

Verified by a 3-skeptic adversarial workflow (cost arithmetic, ToolNormalizer, seam/fence/spec).
Cost dimension clean; fixed the findings it surfaced: the Gemini reshape was over-stripping
`anyOf` (destroying unions) and the numeric/string bound keywords — now kept; normalizeToolCall
now rejects empty ids; and llm-provider-seam.md §6 is clarified to state `inputTokens` is net of
cache reads (the OpenAI/DeepSeek adapter subtracts the cached subset), matching the CostTracker.

+20 llm tests (39 total). Full gate green; seam fence airtight; db drift gate unchanged.

Refs: ADR-0011

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Reviewer's Guide

Freezes the provider-agnostic LLMProvider seam in @relavium/llm using Zod schemas and a TS interface, adds a ToolNormalizer that maps canonical ToolDef into provider-specific wire formats (including a Gemini OpenAPI-subset reshaper and synthesized tool-call IDs), introduces a CostTracker built on a canonical micro-cent pricing table, and wires these into the package’s public API and docs while updating roadmap status and shared content types.

Sequence diagram for ToolNormalizer and Gemini tool-call IDs

sequenceDiagram
  participant Caller
  participant ToolNormalizer as ToolNormalizer_toWire
  participant GeminiIds as GeminiToolCallIds
  participant Adapter
  participant Provider as Gemini

  Caller->>ToolNormalizer: toWire(ToolDef, providerId)
  alt providerId == gemini
    ToolNormalizer->>ToolNormalizer: reshapeForGemini(parameters, name)
    ToolNormalizer-->>Caller: GeminiToolWire
  else other providers
    ToolNormalizer-->>Caller: OpenAiToolWire / AnthropicToolWire
  end

  Caller->>GeminiIds: synthesize(name)
  GeminiIds-->>Caller: id
  Caller->>Adapter: send functionCall(id, name, args)
  Adapter->>Provider: functionCall
  Provider-->>Adapter: functionResponse(name, result)
  Adapter->>GeminiIds: resolveResponse(name)
  GeminiIds-->>Adapter: id
  Adapter->>Caller: normalizeToolCall(provider, { id, name, args })
Loading

File-Level Changes

Change Details Files
Introduce the frozen LLMProvider seam types in @relavium/llm and re-export shared content/stop-reason primitives.
  • Define ProviderId, LlmRequest, LlmMessage, ToolDef, Usage, CapabilityFlags, LlmError, LlmResult, StreamChunk as Zod schemas plus the LlmProvider interface, ensuring no vendor SDK types cross the seam.
  • Use JSONSchema7 for ToolDef.parameters and a local nonEmptyString/nonNegativeInt to keep the seam self-contained while relying on shared StopReasonSchema and ContentPartSchema.
  • Add shared ContentPartSchema and AbortSignalLike in @relavium/shared and re-export them (and StopReason/AbortSignalLike) from the @relavium/llm public index.
packages/llm/src/types.ts
packages/llm/src/types.test.ts
packages/llm/src/index.ts
packages/shared/src/content.ts
packages/shared/src/content.test.ts
packages/shared/src/index.ts
Add ToolNormalizer to map canonical ToolDef to provider-specific tool wiring and normalize tool-call responses, including Gemini-specific handling.
  • Define provider-agnostic OpenAiToolWire, AnthropicToolWire, and GeminiToolWire shapes and a toWire() function that builds the appropriate shape per ProviderId.
  • Implement reshapeForGemini() to strip JSON Schema down to the Gemini-supported OpenAPI subset, preserving anyOf and numeric/string constraints, and throwing ToolSchemaError on unsupported constructs like $ref or invalid roots.
  • Introduce GeminiToolCallIds to synthesize stable tool-call IDs for Gemini and normalizeToolCall() to validate and fold provider tool calls into canonical tool_call ContentPart, with tests covering edge cases and error paths.
packages/llm/src/tool-normalizer.ts
packages/llm/src/tool-normalizer.test.ts
packages/llm/src/errors.ts
Introduce pricing table and CostTracker for canonical cost computation in micro-cents based on usage and model pricing.
  • Define MODEL_PRICING as the canonical mapping from canonical model ids to ModelPricing, using integer micro-cents per MTok and covering Anthropic, OpenAI, Gemini, and DeepSeek models with placeholder prices where verification is pending.
  • Implement priceModel() to look up pricing and throw UnknownModelError on unknown models, and cost() to compute per-usage micro-cent costs across input, output, cache read, and cache write token classes with a single rounding point.
  • Add CostTracker class and CostUpdate type to accumulate per-attempt costs and expose cumulative micro-cent totals, wiring these into the public @relavium/llm surface and adding tests for pricing, unknown models, and cost accumulation.
packages/llm/src/pricing.ts
packages/llm/src/cost-tracker.ts
packages/llm/src/cost-tracker.test.ts
packages/llm/src/errors.ts
packages/llm/src/index.ts
Scaffold @relavium/llm as a real package and expose the seam, pricing, cost, and tool-normalization APIs.
  • Add @relavium/llm package.json with build/test/lint scripts, type-only @types/json-schema dev dependency, and workspace wiring.
  • Add tsconfig.json and tsconfig.build.json for @relavium/llm to enforce a platform-free, typecheck-only config with no ambient Node/DOM types.
  • Curate the @relavium/llm public index exports to include only seam schemas/types, shared re-exports, config error types, pricing/cost APIs, and ToolNormalizer utilities.
packages/llm/package.json
packages/llm/tsconfig.json
packages/llm/tsconfig.build.json
pnpm-workspace.yaml
pnpm-lock.yaml
packages/llm/src/index.ts
Update roadmap and reference docs to reflect Phase 1 status, seam specifics, and cost/usage conventions, plus track pricing follow-ups.
  • Mark Phase 1 Wave 0 (1.L.0) as done and Phase 1 as in progress, describe Wave 1 lanes (1.A seam types and 1.L parser), and adjust immediate-next-steps narrative.
  • Clarify in the llm-provider-seam reference that inputTokens is net of cache, detailing how each provider’s native usage fields map into input/cacheRead/cacheWrite tokens for CostTracker.
  • Update @relavium/llm README to describe the immovable seam contract, current status (1.A done; 1.B/1.E etc. forthcoming), and how adapters will be layered; add deferred tasks to verify non-Anthropic pricing and decide on a model_catalog cache-write column.
  • Adjust roadmap/deferred-tasks and related docs to point to pricing.ts as source of truth and to record verification tasks for OpenAI/Gemini/DeepSeek pricing and DB schema alignment.
docs/roadmap/current.md
docs/roadmap/phases/phase-1-engine-and-llm.md
docs/reference/shared-core/llm-provider-seam.md
docs/roadmap/deferred-tasks.md
packages/llm/README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Linter diff in the way? Review this PR in Change Stack to focus on meaningful changes and expand context only when needed.

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Scaffolds @relavium/llm, adds the provider-agnostic LLM seam (types, tool normalization, pricing/cost tracking, seam errors), updates shared content contracts, adds tests, packaging/tsconfigs, and documentation/roadmap entries marking Phase 1 in-progress.

Changes

LLM Provider Seam Implementation

Layer / File(s) Summary
Shared content normalization
packages/shared/src/content.ts, packages/shared/src/content.test.ts, packages/shared/src/index.ts
Defines ContentPartSchema discriminated union (text/tool_call/tool_result) and AbortSignalLike, with tests and re-export from shared package.
Core LLM seam types & tests
packages/llm/src/types.ts, packages/llm/src/types.test.ts
Adds Zod runtime schemas and TS types for ProviderId, ToolDef, messages, LlmRequest/LlmResult, Usage, LlmErrorKind, StreamChunk union, and LlmProvider interface; includes schema and type-level tests validating seam boundaries.
Configuration & validation errors
packages/llm/src/errors.ts
Introduces discriminated LlmConfigError base plus UnknownModelError and ToolSchemaError used in pricing and tool-schema validation.
Tool normalization across providers
packages/llm/src/tool-normalizer.ts, packages/llm/src/tool-normalizer.test.ts
Implements toWire() conversions, reshapeForGemini() OpenAPI-subset reshaper, GeminiToolCallIds id synthesis/resolution, and normalizeToolCall() canonicalization, with tests covering errors and edge cases.
Pricing data & cost tracking
packages/llm/src/pricing.ts, packages/llm/src/cost-tracker.ts, packages/llm/src/cost-tracker.test.ts
Adds canonical MODEL_PRICING (micro-cents per M-token), isCanonicalModelId, KNOWN_MODEL_IDS; implements priceModel(), cost() with per-class rounding and cache-write defaults, and CostTracker accumulator with tests for rounding, cache handling, and cumulative totals.
Module entry point & public API
packages/llm/src/index.ts
Freezes and re-exports seam runtime schemas/types, shared content contracts, error classes, pricing/cost APIs, and tool-normalization utilities as the @relavium/llm surface.
Build configuration & packaging
packages/llm/package.json, packages/llm/tsconfig.json, packages/llm/tsconfig.build.json, pnpm-workspace.yaml
Adds package metadata, build/typecheck tsconfigs (typecheck-only and build emit), workspace catalog entry for @types/json-schema, and npm scripts for build/lint/test/typecheck.
Documentation & roadmap updates
docs/reference/shared-core/llm-provider-seam.md, docs/roadmap/current.md, docs/roadmap/deferred-tasks.md, docs/roadmap/phases/phase-1-engine-and-llm.md, packages/llm/README.md, README.md
Clarifies canonical billing semantics (inputTokens net of cache; cacheRead/cacheWrite disjoint), marks Phase 1 as in-progress with Wave 0 complete and Wave 1 lanes defined, adds deferred tasks for pricing/DB alignment, and replaces the packages/llm README placeholder with seam documentation.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant LlmProvider
  participant Adapter
  participant ToolNormalizer
  participant CostTracker
  Client->>LlmProvider: generate(LlmRequest, apiKey)
  LlmProvider->>Adapter: send vendor request (ToolWire)
  Adapter->>ToolNormalizer: provider response (tool payload)
  ToolNormalizer->>ToolNormalizer: normalizeToolCall() / reshapeForGemini
  ToolNormalizer-->>LlmProvider: ContentPart[] (canonical tool_call/tool_result)
  LlmProvider->>CostTracker: record(modelId, Usage)
  CostTracker-->>LlmProvider: CostUpdate
  LlmProvider-->>Client: LlmResult (content, stopReason, usage)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • HodeTech/Relavium#1: Adds ContentPartSchema/AbortSignalLike and the initial shared package export surface related to the shared contracts used here.

Poem

🐰 Tiny paws tap keys to freeze the seam just right,
Tokens counted in micro-cents through day and night,
Tools reshaped for Gemini, OpenAI, Anthropic too,
Errors typed and tests run clean — the contract holds true,
A carrot for the reviewer, the rabbit cheers anew!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main changes: delivery of Wave 1 seam types (LLMProvider/ToolNormalizer/CostTracker) identified by their phase/workstream codes (1.A/1.E/1.B), which directly matches the core objectives.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Copy link
Copy Markdown

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 scaffolds the @relavium/llm package, establishing the provider-agnostic LLMProvider seam types, a CostTracker with a canonical model-pricing table, and a ToolNormalizer to reshape tool definitions for various providers. The review feedback highlights several robustness improvements: adding defensive type checks on untrusted provider tool-call IDs and names to prevent runtime crashes, using hasOwnProperty for model pricing lookups to avoid prototype property collisions, and refining the custom Zod validator for tool parameters to explicitly reject arrays.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +202 to +208
if (extracted.id.length === 0 || extracted.name.length === 0) {
throw new ToolSchemaError(
provider,
extracted.name.length === 0 ? '(unnamed)' : extracted.name,
'a tool call must carry a non-empty id and name',
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Directly accessing .length on extracted.id and extracted.name assumes they are always defined strings. However, since these values originate from untrusted provider SDK responses, they could be undefined, null, or of an unexpected type at runtime, leading to a TypeError crash. Adding defensive type and presence checks prevents runtime exceptions.

  if (
    typeof extracted.id !== 'string' ||
    extracted.id.length === 0 ||
    typeof extracted.name !== 'string' ||
    extracted.name.length === 0
  ) {
    throw new ToolSchemaError(
      provider,
      typeof extracted.name === 'string' && extracted.name.length > 0 ? extracted.name : '(unnamed)',
      'a tool call must carry a non-empty id and name',
    );
  }

Comment on lines +12 to +20
export function priceModel(modelId: string): ModelPricing {
const pricing: ModelPricing | undefined = (MODEL_PRICING as Record<string, ModelPricing>)[
modelId
];
if (pricing === undefined) {
throw new UnknownModelError(modelId, KNOWN_MODEL_IDS);
}
return pricing;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

Using direct property lookup MODEL_PRICING[modelId] can lead to unexpected behavior or bypasses if modelId matches built-in Object.prototype properties (such as 'toString', 'valueOf', or 'constructor'). Since modelId is a dynamic string (potentially coming from user-authored YAML or external inputs), it is safer to verify ownership using Object.prototype.hasOwnProperty.call before accessing the pricing record.

export function priceModel(modelId: string): ModelPricing {
  if (!Object.prototype.hasOwnProperty.call(MODEL_PRICING, modelId)) {
    throw new UnknownModelError(modelId, KNOWN_MODEL_IDS);
  }
  return (MODEL_PRICING as Record<string, ModelPricing>)[modelId]!;
}

Comment thread packages/llm/src/types.ts Outdated
Comment on lines +37 to +39
parameters: z.custom<JSONSchema7>((value) => typeof value === 'object' && value !== null, {
message: 'parameters must be a JSON-Schema object',
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The current custom validator for parameters only checks typeof value === 'object' && value !== null, which incorrectly accepts arrays (since typeof [] === 'object'). Since tool parameters must be a JSON-Schema object, arrays should be explicitly rejected by adding an !Array.isArray(value) check.

Suggested change
parameters: z.custom<JSONSchema7>((value) => typeof value === 'object' && value !== null, {
message: 'parameters must be a JSON-Schema object',
}),
parameters: z.custom<JSONSchema7>((value) => typeof value === 'object' && value !== null && !Array.isArray(value), {
message: 'parameters must be a JSON-Schema object',
}),

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In ToolDefSchema, the parameters validator only checks for typeof value === 'object' && value !== null, which will allow arrays despite the comment and JSON Schema expectations; consider tightening this (e.g., rejecting arrays explicitly) so only object schemas are accepted at the seam.
  • In the Gemini reshape logic, GEMINI_SAFE_FORMATS includes 'enum' as a format value, which doesn’t correspond to a standard JSON Schema/Gemini format; it might be worth double‑checking this list against Gemini’s documented formats to avoid silently passing through unsupported values.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ToolDefSchema`, the `parameters` validator only checks for `typeof value === 'object' && value !== null`, which will allow arrays despite the comment and JSON Schema expectations; consider tightening this (e.g., rejecting arrays explicitly) so only object schemas are accepted at the seam.
- In the Gemini reshape logic, `GEMINI_SAFE_FORMATS` includes `'enum'` as a `format` value, which doesn’t correspond to a standard JSON Schema/Gemini format; it might be worth double‑checking this list against Gemini’s documented formats to avoid silently passing through unsupported values.

## Individual Comments

### Comment 1
<location path="packages/llm/src/types.ts" line_range="34-40" />
<code_context>
+export const ToolDefSchema = z.object({
+  name: nonEmptyString,
+  description: z.string().optional(),
+  parameters: z.custom<JSONSchema7>((value) => typeof value === 'object' && value !== null, {
+    message: 'parameters must be a JSON-Schema object',
+  }),
+});
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Tighten the ToolDef.parameters guard to reject arrays and booleans explicitly.

This predicate accepts any non-null object, including arrays, but later logic assumes a record-like JSON Schema root. Consider tightening it to something like `isRecord(value)` (`typeof value === 'object' && value !== null && !Array.isArray(value)`) or, if needed, explicitly allow only object/boolean roots. This avoids accidentally accepting array schemas and keeps `parameters` aligned with the expected shape.

```suggestion
export const ToolDefSchema = z.object({
  name: nonEmptyString,
  description: z.string().optional(),
  parameters: z.custom<JSONSchema7>(
    (value): value is JSONSchema7 =>
      typeof value === 'object' && value !== null && !Array.isArray(value),
    {
      message: 'parameters must be a JSON-Schema object with an object root',
    },
  ),
});
```
</issue_to_address>

### Comment 2
<location path="packages/llm/src/tool-normalizer.ts" line_range="94-95" />
<code_context>
+]);
+
+/** `format` values Gemini accepts; an unsupported format is dropped, not sent. */
+const GEMINI_SAFE_FORMATS = new Set(['enum', 'date-time', 'int32', 'int64', 'float', 'double']);
+
+function reshapeNode(node: unknown, toolName: string): unknown {
</code_context>
<issue_to_address>
**suggestion:** Revisit `'enum'` in GEMINI_SAFE_FORMATS; it’s not a `format` keyword and may never appear here.

Since `GEMINI_ALLOWED_KEYWORDS` already covers `'enum'`, it will be preserved independently of `format`. Including `'enum'` in `GEMINI_SAFE_FORMATS` implies `format` might be set to `'enum'`, which isn’t a standard `format` and could mislead callers. I suggest removing `'enum'` from this set, or adding a brief comment if there’s a deliberate reason to allow a non-standard `format` value here.

```suggestion
/** `format` values Gemini accepts; an unsupported format is dropped, not sent. */
const GEMINI_SAFE_FORMATS = new Set(['date-time', 'int32', 'int64', 'float', 'double']);
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/llm/src/types.ts
Comment thread packages/llm/src/tool-normalizer.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
packages/llm/src/tool-normalizer.test.ts (1)

28-28: ⚡ Quick win

Replace unsafe as casts in tests with narrowing helpers.

These assertions bypass the strict typing guarantees and violate the TS guideline. Prefer shape checks (in) + local assertion helpers so the tests stay type-safe without forced casts.

Example pattern
-const wire = toWire(toolDef, 'anthropic') as AnthropicToolWire;
+const wire = toWire(toolDef, 'anthropic');
+expect('input_schema' in wire).toBe(true);
+if (!('input_schema' in wire)) throw new Error('expected AnthropicToolWire');

As per coding guidelines: “Use TypeScript with strict mode enabled; no any types and no unsafe type assertions with as”.

Also applies to: 37-37, 46-46, 76-76, 96-96, 117-117, 131-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/llm/src/tool-normalizer.test.ts` at line 28, The test uses unsafe
"as" casts (e.g., const wire = toWire(toolDef, 'anthropic') as
AnthropicToolWire) which bypass TypeScript's strict checks; replace these with
runtime narrowing helpers and shape checks: add type-guard functions (e.g.,
isAnthropicToolWire(obj): obj is AnthropicToolWire) that use "in" or property
checks, call the guard right after calling toWire and assert its truth (or
throw) before treating the value as AnthropicToolWire, and apply the same
pattern for the other casts referenced (lines for other tool wires); this
preserves type-safety in tool-normalizer.test.ts while avoiding unsafe "as"
assertions.
packages/llm/src/tool-normalizer.ts (1)

97-140: ⚡ Quick win

Reduce reshapeNode complexity to satisfy the quality gate.

Line 97 is over the configured cognitive complexity limit. Splitting format/properties/array-key handling into helpers should clear the gate and make future keyword changes safer.

Refactor direction
-function reshapeNode(node: unknown, toolName: string): unknown {
+function reshapeNode(node: unknown, toolName: string): unknown {
   if (Array.isArray(node)) {
     return node.map((entry) => reshapeNode(entry, toolName));
   }
   if (!isRecord(node)) return node;
   if ('$ref' in node) {
     throw new ToolSchemaError('gemini', toolName, '`$ref` is not expressible in the Gemini schema subset');
   }
-  const out: Record<string, unknown> = {};
-  for (const [key, value] of Object.entries(node)) {
-    ...
-  }
-  return out;
+  return reshapeObjectNode(node, toolName);
 }

(Extract reshapeObjectNode, reshapeFormatKeyword, reshapePropertiesKeyword, and reshapeNestedSchemaKeyword.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/llm/src/tool-normalizer.ts` around lines 97 - 140, reshapeNode is
exceeding cognitive complexity; extract keyword-specific logic into small
helpers to simplify it. Create reshapeFormatKeyword(nodeValue, toolName) to
handle 'format' filtering (using GEMINI_SAFE_FORMATS),
reshapePropertiesKeyword(propertiesValue, toolName) to iterate and call
reshapeNode for each property, and reshapeNestedSchemaKeyword(value, toolName)
to handle 'items' and 'anyOf' by delegating to reshapeNode; then refactor
reshapeNode to call these helpers for keys 'format', 'properties', 'items' and
'anyOf', keep the '$ref' check and the final passthrough assignment, and ensure
helper names are used (reshapeObjectNode optional) so the main reshapeNode
becomes a simple dispatcher and passes the quality gate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/roadmap/phases/phase-1-engine-and-llm.md`:
- Around line 3-5: Update the roadmap wording that currently states Wave 1 is
“next” to reflect that Wave‑1 workstreams are underway; replace the phrase
describing Wave 1 as “next” with “in progress” (or equivalent) and note that PR
`#7` (2026-06-05) landed Wave‑1 workstreams 1.A/1.B/1.E so the line referencing
Wave 1, 1.A, and 1.L is accurate and up-to-date.

In `@packages/llm/src/cost-tracker.ts`:
- Around line 13-15: Replace the unsafe cast when indexing MODEL_PRICING:
instead of casting MODEL_PRICING as Record<string, ModelPricing>, first narrow
modelId with a type guard like `if (modelId in MODEL_PRICING)` (or a custom
`modelId is CanonicalModelId` check) and then assign `pricing =
MODEL_PRICING[modelId]` without `as`; this preserves strict typing for
ModelPricing and avoids weakening MODEL_PRICING’s type.

In `@packages/llm/src/pricing.ts`:
- Line 146: Create and export a type-guard function like
isCanonicalModelId(value: string): value is CanonicalModelId that checks
membership against Object.keys(MODEL_PRICING) (e.g., build a Set of keys and
test value). Replace KNOWN_MODEL_IDS = Object.keys(MODEL_PRICING) as readonly
CanonicalModelId[] with KNOWN_MODEL_IDS =
Object.keys(MODEL_PRICING).filter(isCanonicalModelId) so the filter uses the
predicate and yields CanonicalModelId[] without an unsafe cast. Then import and
use isCanonicalModelId in packages/llm/src/cost-tracker.ts to narrow modelId
before indexing MODEL_PRICING[modelId] (remove the (MODEL_PRICING as
Record<string, ModelPricing>)[modelId] cast).

In `@packages/llm/src/tool-normalizer.ts`:
- Around line 146-150: After calling reshapeNode in reshapeForGemini, also
validate that the reshaped schema is an object-root (not just a record shape);
update the check to throw ToolSchemaError('gemini', toolName, ...) when either
!isRecord(reshaped) or reshaped.type !== 'object' so schemas like { type:
'string' } are rejected; locate reshapeForGemini and adjust the post-reshape
validation (currently using isRecord) to include the type === 'object' guard
referencing reshapeNode, isRecord, and ToolSchemaError.

In `@packages/llm/src/types.ts`:
- Around line 37-39: The predicate for ToolDef.parameters currently accepts any
non-null object (including arrays); update the z.custom<JSONSchema7> predicate
in packages/llm/src/types.ts (the parameters field) to explicitly reject arrays
by adding a check like !Array.isArray(value) so only plain objects pass, and
keep or adjust the error message 'parameters must be a JSON-Schema object'
accordingly.
- Around line 122-123: The `signal` schema uses z.custom without a predicate so
it accepts anything; update the `signal` entry in packages/llm/src/types.ts to
use a type-guard predicate that validates the shape of AbortSignalLike from
`@relavium/shared` (check that value is an object, has a boolean `aborted`
property and `addEventListener`/`removeEventListener` methods) by replacing
`z.custom<AbortSignalLike>().optional()` with `z.custom<AbortSignalLike>((v): v
is AbortSignalLike => typeof v === 'object' && v !== null && typeof (v as
any).aborted === 'boolean' && typeof (v as any).addEventListener === 'function'
&& typeof (v as any).removeEventListener === 'function').optional()`, keeping
the `signal` name and optionality intact.

---

Nitpick comments:
In `@packages/llm/src/tool-normalizer.test.ts`:
- Line 28: The test uses unsafe "as" casts (e.g., const wire = toWire(toolDef,
'anthropic') as AnthropicToolWire) which bypass TypeScript's strict checks;
replace these with runtime narrowing helpers and shape checks: add type-guard
functions (e.g., isAnthropicToolWire(obj): obj is AnthropicToolWire) that use
"in" or property checks, call the guard right after calling toWire and assert
its truth (or throw) before treating the value as AnthropicToolWire, and apply
the same pattern for the other casts referenced (lines for other tool wires);
this preserves type-safety in tool-normalizer.test.ts while avoiding unsafe "as"
assertions.

In `@packages/llm/src/tool-normalizer.ts`:
- Around line 97-140: reshapeNode is exceeding cognitive complexity; extract
keyword-specific logic into small helpers to simplify it. Create
reshapeFormatKeyword(nodeValue, toolName) to handle 'format' filtering (using
GEMINI_SAFE_FORMATS), reshapePropertiesKeyword(propertiesValue, toolName) to
iterate and call reshapeNode for each property, and
reshapeNestedSchemaKeyword(value, toolName) to handle 'items' and 'anyOf' by
delegating to reshapeNode; then refactor reshapeNode to call these helpers for
keys 'format', 'properties', 'items' and 'anyOf', keep the '$ref' check and the
final passthrough assignment, and ensure helper names are used
(reshapeObjectNode optional) so the main reshapeNode becomes a simple dispatcher
and passes the quality gate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9402e818-7816-4f60-81b9-229baa13cbdb

📥 Commits

Reviewing files that changed from the base of the PR and between 21b2f9c and 7ca81b3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • docs/reference/shared-core/llm-provider-seam.md
  • docs/roadmap/current.md
  • docs/roadmap/deferred-tasks.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • packages/llm/README.md
  • packages/llm/package.json
  • packages/llm/src/cost-tracker.test.ts
  • packages/llm/src/cost-tracker.ts
  • packages/llm/src/errors.ts
  • packages/llm/src/index.ts
  • packages/llm/src/pricing.ts
  • packages/llm/src/tool-normalizer.test.ts
  • packages/llm/src/tool-normalizer.ts
  • packages/llm/src/types.test.ts
  • packages/llm/src/types.ts
  • packages/llm/tsconfig.build.json
  • packages/llm/tsconfig.json
  • packages/shared/src/content.test.ts
  • packages/shared/src/content.ts
  • packages/shared/src/index.ts
  • pnpm-workspace.yaml

Comment thread docs/roadmap/phases/phase-1-engine-and-llm.md Outdated
Comment thread packages/llm/src/cost-tracker.ts Outdated
Comment thread packages/llm/src/pricing.ts Outdated
Comment thread packages/llm/src/tool-normalizer.ts
Comment thread packages/llm/src/types.ts Outdated
Comment thread packages/llm/src/types.ts Outdated
cemililik and others added 2 commits June 6, 2026 11:10
Address PR #7 review on the @relavium/llm seam (1.A/1.B/1.E):

- types.ts: ToolDef.parameters now rejects arrays/non-objects and the
  request `signal` is validated structurally against AbortSignalLike, so
  malformed input is caught at the seam instead of failing later at the
  provider call or when the signal is observed.
- tool-normalizer.ts: reshapeForGemini requires an object root (a
  primitive-root schema now throws, matching its docstring); a nullable
  `type: [x, "null"]` union collapses to a scalar type + `nullable: true`,
  and an inexpressible non-null union throws ToolSchemaError, instead of
  shipping an invalid Gemini payload. Dropped the bogus `enum` entry from
  GEMINI_SAFE_FORMATS.
- pricing.ts / cost-tracker.ts: replace two unsafe `as` casts with an
  isCanonicalModelId type guard (Set-of-own-keys, so it is also immune to
  prototype-chain keys); clarify that cacheWrite is in-code-only with no
  model_catalog column.
- tests: drop unsafe `as ...Wire` casts; add reject/accept cases for array
  params, signal, primitive-root, and nullable/inexpressible type unions.

No vendor SDK type crosses the seam.

Refs: ADR-0011
Phase 1 · 1.A/1.B/1.E

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the status blurbs so they reflect reality: Wave 1 is in progress
rather than "next", with the seam workstreams 1.A/1.B/1.E in review under
the open PR #7 (2026-06-05). Deliberately "in review", not "landed" — PR #7
is not yet merged to main, and a workstream is only Done once its PR merges.

Phase 1 · 1.A/1.B/1.E

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/llm/src/tool-normalizer.test.ts (1)

159-159: ⚡ Quick win

Prefer a type guard over the type assertion.

The as Record<string, Record<string, unknown>> assertion narrows unknown without runtime validation. As per coding guidelines, prefer type guards over unsafe as casts. The source code already uses an isRecord helper for this pattern.

♻️ Proposed refactor using a type guard
-  const props = reshaped['properties'] as Record<string, Record<string, unknown>>;
+  const props = reshaped['properties'];
+  if (typeof props !== 'object' || props === null || Array.isArray(props)) {
+    throw new Error('expected properties to be a record');
+  }
+  const typedProps = props as Record<string, Record<string, unknown>>;
-  expect(props['opt']).toEqual({ type: 'string', nullable: true });
+  expect(typedProps['opt']).toEqual({ type: 'string', nullable: true });

Alternatively, import and use the isRecord helper from the source code for cleaner guard logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/llm/src/tool-normalizer.test.ts` at line 159, Replace the unsafe
assertion "const props = reshaped['properties'] as Record<string, Record<string,
unknown>>" with a runtime type guard using the existing isRecord helper: check
that reshaped['properties'] satisfies isRecord and that each value is a record
(or use nested isRecord checks) before assigning to props; import isRecord if
not already imported and handle the failure branch (e.g., throw or fail the
test) so props is properly narrowed without using "as".

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/llm/src/tool-normalizer.test.ts`:
- Line 159: Replace the unsafe assertion "const props = reshaped['properties']
as Record<string, Record<string, unknown>>" with a runtime type guard using the
existing isRecord helper: check that reshaped['properties'] satisfies isRecord
and that each value is a record (or use nested isRecord checks) before assigning
to props; import isRecord if not already imported and handle the failure branch
(e.g., throw or fail the test) so props is properly narrowed without using "as".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c2e2dfb7-1622-48ef-b5fe-e92096a32dc6

📥 Commits

Reviewing files that changed from the base of the PR and between 7ca81b3 and ef4eee7.

📒 Files selected for processing (8)
  • README.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • packages/llm/src/cost-tracker.ts
  • packages/llm/src/pricing.ts
  • packages/llm/src/tool-normalizer.test.ts
  • packages/llm/src/tool-normalizer.ts
  • packages/llm/src/types.test.ts
  • packages/llm/src/types.ts
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • packages/llm/src/cost-tracker.ts
  • packages/llm/src/pricing.ts
  • packages/llm/src/tool-normalizer.ts
  • packages/llm/src/types.test.ts
  • packages/llm/src/types.ts

cemililik and others added 2 commits June 6, 2026 11:19
Address the SonarQube findings on the seam:

- tool-normalizer.ts: split reshapeNode (cognitive complexity 29 → well under
  the 15 limit) into small per-keyword helpers (reshapeFormat / reshapeTypeArray
  / reshapeProperties / reshapeKeyword) behind a thin dispatch loop. Behaviour is
  unchanged — all 42 tests stay green. Also invert the toWire `desc` ternary to
  drop the negated-condition readability smell.
- pricing.ts: drop the redundant `.0` zero-fractions in the usd(...) literals
  (5.0 → 5, 25.0 → 25, …); the priced values are identical.

Refs: ADR-0011
Phase 1 · 1.B/1.E

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A 7-dimension adversarial review of PR #7 surfaced 8 confirmed findings (1 medium, 5 low,
2 info; 3 false-positives filtered). No blockers — all hardening/polish on the 1.E
ToolNormalizer plus test coverage. Fixed:

- [MED security] reshapeForGemini walked an untrusted tool-parameters JSON-Schema with
  UNBOUNDED recursion — a deeply-nested schema overflowed the stack with a raw RangeError.
  Added a MAX_SCHEMA_DEPTH (100) cap that raises a typed ToolSchemaError instead.
- [LOW security] a property literally named `__proto__` under `properties` was assigned via
  bracket syntax, invoking the prototype setter (silently dropping the param + mutating the
  emitted node's prototype — not global pollution). reshapeProperties now writes members with
  Object.defineProperty, so `__proto__` survives as an own key.
- [LOW correctness] the db9206c tightening over-rejected: a no-argument tool's `parameters: {}`
  (a typeless object root, valid for Anthropic/OpenAI/DeepSeek) threw for Gemini. reshapeForGemini
  now defaults a typeless root to `type: 'object'`, throwing only on a non-object root.
- [LOW maintainability] a `type: ['string','null']`-derived `nullable: true` could be clobbered by
  a contradictory verbatim sibling `nullable`; the type-union null is now authoritative.

Tests (+ coverage): cases for the depth cap, the `__proto__` own-key, the `{}` default, the nullable
precedence, and the primitive-node passthrough branch (`items: true`); deepened the AbortSignalLike
validation test (partial-object + wrong-typed `aborted` rejection); added a MODEL_PRICING
table-invariant test (keys = KNOWN_MODEL_IDS, every catalog-projection field complete + integer).
Suite: 48 green.

Review nitpick: replaced the unsafe `reshaped['properties'] as Record<...>` test assertions with a
runtime `recordAt` guard (isRecord-based), removing the unsafe `as`.

Full gate green; seam fence airtight; no DB drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/llm/src/cost-tracker.test.ts`:
- Line 84: The test's key-order assertion uses Array.sort() without a comparator
which can be unstable; update the assertion to sort both sides with an explicit
locale-aware comparator using String.prototype.localeCompare so both
Object.keys(MODEL_PRICING) and [...KNOWN_MODEL_IDS] are sorted via (a, b) =>
a.localeCompare(b) before comparison; locate the assertion in
packages/llm/src/cost-tracker.test.ts that references MODEL_PRICING and
KNOWN_MODEL_IDS and change the .sort() calls accordingly.

In `@packages/llm/src/tool-normalizer.test.ts`:
- Around line 37-43: The test currently uses unsafe casts to JSONSchema7; update
the parseSchema implementation and the other literal cast so we validate at
runtime instead of asserting types. For parseSchema, change the boundary cast
JSON.parse(json) as JSONSchema7 to parse into unknown, perform a runtime check
that the result is an object (and optionally has expected root keys like "type"
or "$schema") and only then return it typed as JSONSchema7; for the remaining
object literal cast (the `} as JSONSchema7` usage) remove the `as` cast and
either use TypeScript's `satisfies JSONSchema7` for a static literal or add a
small runtime guard when the value is derived, mirroring the check used by
parseSchema. Ensure you update references to parseSchema and the specific object
literal to use these runtime guards or `satisfies` so no direct `as JSONSchema7`
assertions remain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8eafbe87-56d3-4931-825d-1bb0af28858a

📥 Commits

Reviewing files that changed from the base of the PR and between 328f32a and 52e2b8e.

📒 Files selected for processing (4)
  • packages/llm/src/cost-tracker.test.ts
  • packages/llm/src/tool-normalizer.test.ts
  • packages/llm/src/tool-normalizer.ts
  • packages/llm/src/types.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/llm/src/types.test.ts
  • packages/llm/src/tool-normalizer.ts

Comment thread packages/llm/src/cost-tracker.test.ts Outdated
Comment thread packages/llm/src/tool-normalizer.test.ts
cemililik and others added 2 commits June 6, 2026 12:17
- cost-tracker.test.ts: give the MODEL_PRICING key-set `.sort()` calls an explicit
  `localeCompare` comparator (Sonar flags a comparator-less sort as type-dependent).
- tool-normalizer.test.ts: use ES2022 `Object.hasOwn(props, '__proto__')` instead of
  `Object.prototype.hasOwnProperty.call(...)`.

Test-only; behavior unchanged. Suite 48 green; gate + fence clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the PR #7 inline nitpick — no direct `as JSONSchema7` assertion remains in the suite:
- parseSchema now parses to `unknown` and narrows with an `isObjectSchema` runtime guard (throws
  on a non-object), instead of `JSON.parse(json) as JSONSchema7`.
- the `$ref` test's literal uses `satisfies JSONSchema7` instead of `as JSONSchema7`.

The L84 sort nitpick was already resolved in 8473263 (localeCompare comparator) — no change.
Test-only; suite 48 green; gate + fence clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jun 6, 2026

Copy link
Copy Markdown

@cemililik cemililik merged commit 4d244a1 into main Jun 6, 2026
7 checks passed
cemililik added a commit that referenced this pull request Jun 6, 2026
The Wave-1 seam trio — 1.A (LLMProvider seam types), 1.B (CostTracker + pricing), 1.E
(ToolNormalizer) — merged in PR #7. Flip their headings to ✅ Done, update the phase Status
line, and point current.md's next-steps at the adapter lane (1.C → {1.D ‖ 1.F}, with 1.I)
running parallel to the 1.L parser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Jun 11, 2026
… merged)

1.K landed in PR #13 (2026-06-11). Reflect it everywhere status lives:
- phase-1: §1.K header ✅ Done; 1.m2 milestone complete (1.B PR #7, 1.K PR #13);
  the dependency-matrix 1.K row gets its Done note; the top status blockquote now
  points past 1.K (next is 1.L, which scaffolds packages/core).
- current.md: the seam policy lane is complete (1.K merged); the engine lane (1.L)
  is the active next step; the multimodal + PR #12 notes no longer call 1.K "next".
- llm-provider-seam.md: the ADR-0030 strip-on-failover is now enforced by 1.K, not
  "not yet exercised — no consumer exists".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Jun 24, 2026
…xt pickup 2.S

2.L (CLI packaging, distribution & install verification) merged via PR #49, closing the last
Phase-3 go/no-go exit criterion (#7) — so the Phase-2 spine is complete and all seven criteria
now hold. Move 2.L from "next pickup" into the Landed/Done list and re-point the next pickup to
2.S (media host-wiring, the first additive lane — off the M3 critical path and the go/no-go).

Audited every status surface; updated only the live trackers and left the static plan views
(Mermaid DAGs, wave/dependency tables, exit-criteria definitions) unchanged per their own
"the plan, not a live tracker" framing.

- current.md: 2.L Done entry + next pickup 2.S; re-tense the NPM_TOKEN maintainer obligation
  ("once 2.L lands" → "now that 2.L has landed (PR #49)").
- phase-2-cli.md: §2.L heading ✅ Done badge; header status line; Remaining-build-order banner;
  drop the 2.L row from the pickup queue and renumber 2.S/2.R/chat/2.J to 1–4; re-tense the
  gate-closing-backbone, 2.K-closed, and 2.S-timing bullets.
- CLAUDE.md, README.md: append the 2.L packaging deliverable to the landed Phase-2 list; next
  pickup 2.S; all seven Phase-3 exit criteria now hold.
- runbooks/release-a-surface.md: Status "draft" → "partial — CLI (npm) flow complete (2.L)".

ADR-0051's "(today a stub)" aside about the runbook is left unedited: ADRs are append-only
(CLAUDE.md rule 9) and it is a harmless point-in-time authoring note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Jun 25, 2026
… status, defer log, concurrency)

Five review findings on the CLI media surface (all low/nit):

- #12 (nit, 3-lens): wireSaveToPort called mkdirSync on every write (blocking sync I/O in an async
  port) while the JSDoc implied once-only. Switch to await mkdir (node:fs/promises) — matches the
  FilesystemMediaStore.put pattern, keeps the port non-blocking — and reword the JSDoc to "every write".
- #13 (nit): TERMINAL_RUN_STATUSES typed Set<RunStatus> (was Set<string>), so a misspelled status is a
  compile error and .has() narrows the closed union.
- #7 (low): createWorkflowModelCatalog deferred silently on a CapabilityFlagsSchema.safeParse failure —
  indistinguishable from "model absent". Add an optional, per-model-deduped, secret-free warn sink
  (model id + Zod issue messages) threaded from run/gate via io.writeErr, so a future schema evolution
  that invalidates previously-valid rows is observable. Still fail-open (FallbackChain backstop).
- #6 (low): document the grace-window soft-delete→unlink resurrection gap (within ADR-0042 §3
  best-effort) so a future graceMs shortening triggers a re-verify-before-delete.
- #5 companion (low, PERF-CONCURRENCY-2): the grace-reclaim and orphan-sweep CAS deletes ran one await
  at a time on the exit path — fan them out with Promise.all (independent unlinks).

Refs: ADR-0042

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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