Skip to content

feat(shared,desktop,providers,core): align connection test and invoca… - #223

Merged
Sun-sunshine06 merged 14 commits into
mainfrom
feat/connection-test-invocation-parity
Apr 24, 2026
Merged

Sun-sunshine06 merged 14 commits into
mainfrom
feat/connection-test-invocation-parity

Conversation

@Sun-sunshine06

Copy link
Copy Markdown
Collaborator

…tion parity (#216)

Summary

Type of change

  • Bug fix
  • New feature
  • Refactor (no behavior change)
  • Documentation
  • Build / CI / tooling
  • Breaking change

Linked issue

Checklist

  • I read docs/VISION.md, docs/PRINCIPLES.md, and CLAUDE.md before starting
  • Commits are signed with DCO (git commit -s)
  • pnpm lint && pnpm typecheck && pnpm test passes locally
  • Added/updated tests for the change
  • Added a changeset (pnpm changeset) if user-visible
  • Updated docs if behavior changed

Dependency additions (if any)

Screenshots / recordings (UI changes)

@github-actions github-actions Bot added area:desktop apps/desktop (Electron shell, renderer) area:core packages/core (generation orchestration) area:providers packages/providers (pi-ai adapter, model calls) labels Apr 24, 2026
…tion parity (#216)

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
@Sun-sunshine06
Sun-sunshine06 force-pushed the feat/connection-test-invocation-parity branch from d8c4287 to ad1136c Compare April 24, 2026 07:52

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Major] modelsHint now disables connection checks for valid static-hint providers — defaultProviderCapabilities() ties supportsModelsEndpoint to modelsHint, and runProviderTest() now treats supportsModelsEndpoint: false as “skip /models entirely”. For an imported anthropic provider with a static model list, connection:v1:test-provider will now return Provider capability profile disables /models, but this wire has no fallback probe even when /v1/models is healthy. Evidence: packages/shared/src/config.ts:188, apps/desktop/src/main/connection-ipc.ts:546, apps/desktop/src/main/connection-ipc.ts:652, apps/desktop/src/main/connection-ipc.ts:900
    Suggested fix:

    const supportsModelsEndpoint =
      entry.capabilities?.supportsModelsEndpoint ?? entry.wire !== 'openai-codex-responses';
    
    const modelDiscoveryMode =
      entry.modelsHint !== undefined
        ? 'static-hint'
        : supportsModelsEndpoint
          ? 'models'
          : 'manual';
  • [Major] Passing resolved default capabilities into inferReasoning() suppresses synthesized reasoning models on chat-based providers — resolveActiveModel() now forwards supportsReasoning: false from built-in OpenAI/OpenRouter capability defaults, and the new early return in inferReasoning() makes that override the existing gpt-5/o*/openai/* heuristics. Any model missing from pi-ai's registry will now be synthesized with reasoning=false on the exact path that previously relied on those heuristics. Evidence: apps/desktop/src/main/provider-settings.ts:230, packages/shared/src/config.ts:260, packages/providers/src/index.ts:222, packages/providers/src/index.test.ts:539
    Suggested fix:

    if (capabilities?.supportsReasoning === true) return true;
    
    const preserveChatHeuristics =
      wire === 'openai-chat' &&
      (isOpenAIOfficial(baseUrl) || /^https:\/\/openrouter\.ai\/api\/v1(\/|$)/.test(baseUrl ?? ''));
    
    if (capabilities?.supportsReasoning === false && !preserveChatHeuristics) return false;

Summary
Review mode: initial. Found 2 major regressions in the new capability plumbing: one false-negative connection test for static-hint providers, and one reasoning-capability override that changes synthesized OpenAI/OpenRouter request shaping. Not found in repo/docs: docs/VISION.md, docs/PRINCIPLES.md in this checkout.

Testing
Not run (automation). pnpm was unavailable initially, and corepack pnpm in this runner is on Node v20.20.2 while the repo requires Node >=22; vitest was also not available from the current install.

open-codesign Bot

@@ -182,14 +187,21 @@ export function defaultProviderCapabilities(
): Required<ProviderCapabilities> {
const supportsModelsEndpoint =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

modelsHint should not implicitly mean supportsModelsEndpoint = false for every wire. With the new runProviderTest() branch, a static-hint anthropic provider now fails connection:v1:test-provider immediately even if /v1/models is healthy.

Suggested fix:

const supportsModelsEndpoint =
  entry.capabilities?.supportsModelsEndpoint ?? entry.wire !== openai-codex-responses;

Comment thread packages/providers/src/index.ts Outdated
baseUrl: string | undefined,
capabilities?: ProviderCapabilities,
): boolean {
if (capabilities?.supportsReasoning !== undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new early return makes the resolved default supportsReasoning: false override the existing gpt-5 / o* / OpenRouter heuristics whenever we synthesize a Pi model. That regresses the unknown-model path for chat-based providers even though the heuristic tests still expect true.

Suggested fix:

if (capabilities?.supportsReasoning === true) return true;
// keep the existing openai-chat heuristics for synthesized models

…asoning heuristics

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Major] Explicit supportsReasoning: false is still ignored for imported providers on official OpenAI/OpenRouter hosts — resolveProviderCapabilities() now always materializes supportsReasoning, so by the time inferReasoning() runs it can no longer tell an explicit false from the built-in default. The new preserveChatHeuristics branch therefore re-enables reasoning for any custom openai-chat provider whose baseUrl is https://api.openai.com/v1 or https://openrouter.ai/api/v1, even when the provider capability profile asked to disable it. That is a reachable path for imported custom providers such as OpenCode's opencode-openrouter entries. Evidence packages/providers/src/index.ts:230, packages/providers/src/index.ts:281, packages/shared/src/config.ts:215, apps/desktop/src/main/imports/opencode-config.ts:68.
    Suggested fix:
    export function inferReasoning(
      providerId: string,
      wire: GenerateOptions['wire'],
      modelId: string,
      baseUrl: string | undefined,
      capabilities?: ProviderCapabilities,
    ): boolean {
      if (capabilities?.supportsReasoning === true) return true;
    
      const preserveChatHeuristics =
        wire === 'openai-chat' && (providerId === 'openai' || providerId === 'openrouter');
    
      if (capabilities?.supportsReasoning === false && !preserveChatHeuristics) return false;
      ...
    }
    
    reasoning: inferReasoning(provider, wire, modelId, baseUrl, capabilities),

Summary
Review mode: follow-up after new commits. Found 1 major regression in the follow-up fix: explicit reasoning opt-outs are still ignored for imported openai-chat providers that point at official OpenAI/OpenRouter base URLs. Not found in repo/docs: docs/VISION.md, docs/PRINCIPLES.md in this checkout.

Testing
Not run (automation). pnpm is not installed in this runner; corepack pnpm resolves, but this checkout has no node_modules, the repo requires Node >=22 while the runner has Node v20.20.2, and turbo is therefore unavailable.

open-codesign Bot

Comment thread packages/providers/src/index.ts Outdated
if (capabilities?.supportsReasoning === true) {
return true;
}
const preserveChatHeuristics =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

resolveProviderCapabilities() now always passes a concrete supportsReasoning boolean downstream, so this hostname check can't distinguish an explicit opt-out from the built-in default. For imported custom providers on https://api.openai.com/v1 / https://openrouter.ai/api/v1, an explicit supportsReasoning: false is ignored here and we still synthesize reasoning: true for gpt-5 / o* model ids.

Suggested fix:

const preserveChatHeuristics =
  wire === 'openai-chat' && (providerId === 'openai' || providerId === 'openrouter');

… endpoints

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Major] Capability propagation still stops at the legacy generate() path — this diff adds capabilities to the generation input and passes them into the legacy completeWithRetry() flow (packages/core/src/index.ts:101, packages/core/src/index.ts:373, apps/desktop/src/main/index.ts:827), but the default-on generateViaAgent() path still ignores them. buildPiModel() hardcodes reasoning: true and thinkingLevel is still derived from reasoningForModel() without considering input.capabilities (packages/core/src/agent.ts:254, packages/core/src/agent.ts:288, packages/core/src/agent.ts:808). As a result, imported openai-chat providers that explicitly set supportsReasoning: false can still hit the same "connection test passes, actual invocation fails" mismatch whenever USE_AGENT_RUNTIME=true.
    Suggested fix:
    import { inferReasoning } from '@open-codesign/providers';
    import type { ProviderCapabilities } from '@open-codesign/shared';
    
    function buildPiModel(
      model: ModelRef,
      wire: WireApi | undefined,
      baseUrl: string | undefined,
      httpHeaders?: Record<string, string>,
      apiKey?: string,
      capabilities?: ProviderCapabilities,
    ): PiModel {
      ...
      const out: PiModel = {
        ...,
        reasoning: inferReasoning(wire, model.modelId, canonicalBase, capabilities, model.provider),
      };
    }
    
    const thinkingLevel =
      input.reasoningLevel ??
      (inferReasoning(input.wire, input.model.modelId, input.baseUrl, input.capabilities, input.model.provider)
        ? reasoningForModel(input.model, input.baseUrl)
        : undefined) ??
      'off';

Summary
Review mode: follow-up after new commits. Found 1 major regression: the new capability propagation does not reach the default-on generateViaAgent() path, so imported providers can still send unsupported reasoning semantics under USE_AGENT_RUNTIME=true.

Testing
Not run (automation). Runner has Node v20.20.2, the repo requires Node 22, and this checkout has no node_modules.

open-codesign Bot

@@ -827,6 +827,7 @@ function registerIpcHandlers(db: Database | null): void {
...(baseUrl !== undefined ? { baseUrl } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This now threads capabilities into the legacy generate() path, but runGenerate() defaults to generateViaAgent() and that path still ignores input.capabilities: buildPiModel() hardcodes reasoning: true and thinkingLevel still comes from reasoningForModel() alone (packages/core/src/agent.ts:254-310, packages/core/src/agent.ts:808-809). Imported openai-chat providers that explicitly set supportsReasoning: false can therefore still hit the same invocation mismatch this PR is trying to eliminate.

Suggested fix:

const piModel = buildPiModel(
  input.model,
  input.wire,
  input.baseUrl,
  input.httpHeaders,
  input.apiKey,
  input.capabilities,
);

const thinkingLevel =
  input.reasoningLevel ??
  (inferReasoning(input.wire, input.model.modelId, input.baseUrl, input.capabilities, input.model.provider)
    ? reasoningForModel(input.model, input.baseUrl)
    : undefined) ??
  'off';

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Major] Builtin OpenAI/OpenRouter rows still force reasoning on when their base URL points at a proxy — inferReasoning() now preserves the official openai-chat heuristics solely from providerId === 'openai' || 'openrouter' (packages/providers/src/index.ts:231), and generateViaAgent() now depends on that branch for the agent runtime (packages/core/src/agent.ts:721). Builtin rows can still be repointed to custom gateways, while their resolved capability profile continues to default supportsReasoning: false (packages/shared/src/config.ts:260, packages/shared/src/config.ts:280). In that configuration, a builtin openai/openrouter row aimed at a proxy like https://api.duckcoding.ai/v1 will still return reasoning: true for gpt-5*, so the “connection test passes, actual invocation fails” mismatch remains for the exact setup this PR is trying to close.
    Suggested fix:
    const preserveChatHeuristics =
      wire === 'openai-chat' &&
      ((providerId === 'openai' && isOpenAIOfficial(baseUrl)) ||
        (providerId === 'openrouter' && isOpenRouterOfficial(baseUrl)));

Summary
Review mode: follow-up after new commits. Found 1 major regression: the new capability-aware reasoning guard still treats builtin OpenAI/OpenRouter providers as “official” after their base URL is repointed to a proxy, so agent-runtime requests can still send unsupported reasoning semantics.

Testing
Not run (automation). Runner has Node v20.20.2, the repo expects Node 22, and this checkout has no node_modules.

open-codesign Bot

Comment thread packages/providers/src/index.ts Outdated
}
const preserveChatHeuristics =
wire === 'openai-chat' && (providerId === 'openai' || providerId === 'openrouter');
if (capabilities?.supportsReasoning === false && !preserveChatHeuristics) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This still keys the “preserve official heuristics” escape hatch on providerId alone. Builtin openai / openrouter rows can be repointed to a proxy base URL, so this branch keeps returning true for gpt-5* even when the resolved capability profile says supportsReasoning: false.

const preserveChatHeuristics =
  wire === 'openai-chat' &&
  ((providerId === 'openai' && isOpenAIOfficial(baseUrl)) ||
    (providerId === 'openrouter' && isOpenRouterOfficial(baseUrl)));

@github-actions github-actions Bot added the area:build Turbo/Vite/Biome/tsconfig toolchain label Apr 24, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Major] The new capabilities plumbing still does not affect registered pi-ai models on the legacy completion path. runModel() now forwards capabilities into completeWithRetry() (packages/core/src/index.ts:373), but complete() only consumes them inside the new synthesizeWireModel(...) branch (packages/providers/src/index.ts:335). When the provider/model pair already exists in pi-ai's registry, that branch is skipped, so applyComment, generateTitle, and generate() with USE_AGENT_RUNTIME=0 still keep the registry model's original reasoning behavior even after a builtin openai/openrouter row is repointed to a proxy.
    Suggested fix:
    let piModel = pi.getModel(model.provider, effectiveModelId);
    if (piModel && opts.wire !== undefined) {
      piModel = {
        ...piModel,
        reasoning: inferReasoning(
          opts.wire,
          effectiveModelId,
          opts.baseUrl,
          opts.capabilities,
          model.provider,
        ),
      };
    }

Summary
Review mode: follow-up after new commits. Found 1 major parity gap: the new provider capability profile is ignored whenever complete() reuses a registered pi-ai model, so the non-agent completion paths can still send unsupported reasoning semantics after a builtin provider is repointed to a proxy. Not found in repo/docs: docs/VISION.md, docs/PRINCIPLES.md.

Testing
Not run (automation). Missing coverage for complete() when pi.getModel() hits and capabilities.supportsReasoning should override the registry model.

open-codesign Bot

Comment thread packages/providers/src/index.ts Outdated
effectiveModelId,
opts.wire,
opts.baseUrl,
opts.capabilities,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This only threads opts.capabilities into the synthesized-model branch. If pi.getModel(model.provider, effectiveModelId) returns a registry model first, the new capability profile is ignored entirely, so the legacy complete() paths can still keep the registry model's original reasoning behavior after a builtin openai/openrouter row is repointed to a proxy.

let piModel = pi.getModel(model.provider, effectiveModelId);
if (piModel && opts.wire !== undefined) {
  piModel = {
    ...piModel,
    reasoning: inferReasoning(
      opts.wire,
      effectiveModelId,
      opts.baseUrl,
      opts.capabilities,
      model.provider,
    ),
  };
}

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Blocker] Missing DCO trailers on commits 726d53fa, b1541355, and 6108a890 — the repo hard-requires Signed-off-by, so this head does not satisfy the PR gate yet.
    Suggested fix:

    git rebase origin/main --exec 'git commit --amend --no-edit -s'
    git push --force-with-lease
  • [Major] complete() drops the builtin OpenAI/OpenRouter fallback when baseUrl is omitted — the new reasoning override now re-runs inferReasoning() against raw opts.baseUrl, so callers that rely on the registry model's official endpoint default can flip registered gpt-5/o* rows back to reasoning: false, evidence packages/providers/src/index.ts:336.
    Suggested fix:

    const effectiveBaseUrl = opts.baseUrl ?? piModel.baseUrl;
    
    piModel = {
      ...piModel,
      reasoning: inferReasoning(
        opts.wire,
        effectiveModelId,
        effectiveBaseUrl,
        opts.capabilities,
        model.provider,
      ),
    };
  • [Major] generateViaAgent() has the same omitted-baseUrl regression for thinkingLevel — buildPiModel() already resolves the builtin public endpoint, but the new gate recomputes inferReasoning() from raw input.baseUrl, so builtin OpenAI/OpenRouter calls can fall back to 'off', evidence packages/core/src/agent.ts:814.
    Suggested fix:

    const effectiveBaseUrl = piModel.baseUrl;
    const thinkingLevel =
      input.reasoningLevel ??
      (inferReasoning(
        input.wire,
        input.model.modelId,
        effectiveBaseUrl,
        input.capabilities,
        input.model.provider,
      )
        ? reasoningForModel(input.model, effectiveBaseUrl)
        : undefined) ??
      'off';

Summary

  • Review mode: follow-up after new commits. Found 3 issues: 3 unsigned commits still violate the repo's DCO requirement, and the new reasoning-capability overrides regress builtin OpenAI/OpenRouter defaults when package callers omit baseUrl. Not found in repo/docs: docs/VISION.md, docs/PRINCIPLES.md.

Testing

  • Not run (automation). Missing coverage for complete() and generateViaAgent() with builtin openai/openrouter, wire set, capabilities.supportsReasoning=false, and omitted baseUrl.

open-codesign Bot

if (piModel && opts.wire !== undefined) {
piModel = {
...piModel,
reasoning: inferReasoning(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This override now depends on raw opts.baseUrl. If a caller relies on the builtin registry model's default official endpoint and omits baseUrl, inferReasoning(...) sees undefined here and disables the official OpenAI/OpenRouter heuristic you just added. opts.baseUrl ?? piModel.baseUrl keeps the fallback intact.

const effectiveBaseUrl = opts.baseUrl ?? piModel.baseUrl;

piModel = {
  ...piModel,
  reasoning: inferReasoning(
    opts.wire,
    effectiveModelId,
    effectiveBaseUrl,
    opts.capabilities,
    model.provider,
  ),
};

const thinkingLevel =
input.reasoningLevel ?? reasoningForModel(input.model, input.baseUrl) ?? 'off';
input.reasoningLevel ??
(inferReasoning(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

buildPiModel() already resolved the builtin public base URL above, but this second inferReasoning(...) call falls back to raw input.baseUrl. For builtin OpenAI/OpenRouter callers that omit baseUrl, that makes thinkingLevel drop to 'off' even though piModel.reasoning stayed enabled. Reuse piModel.baseUrl here so both checks see the same endpoint.

const effectiveBaseUrl = piModel.baseUrl;
const thinkingLevel =
  input.reasoningLevel ??
  (inferReasoning(
    input.wire,
    input.model.modelId,
    effectiveBaseUrl,
    input.capabilities,
    input.model.provider,
  )
    ? reasoningForModel(input.model, effectiveBaseUrl)
    : undefined) ??
  'off';

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Blocker] Four non-merge commits on this head still lack Signed-off-by trailers, which violates the repo's DCO requirement and will block compliance checks, evidence .github/workflows/ci.yml:64. Missing trailers on 726d53fa, 8d68c833, b1541355, and 6108a890.
    Suggested fix:

    git rebase origin/main --exec 'git commit --amend --no-edit -s'
    git push --force-with-lease
  • [Major] The non-agent generate() / applyComment() path still computes the explicit reasoning option without consulting capabilities.supportsReasoning, so builtin openai / openrouter rows repointed at a proxy can keep sending reasoning: high|medium even though this diff now propagates capabilities into completeWithRetry, evidence packages/core/src/index.ts:373.
    Suggested fix:

    import { inferReasoning } from '@open-codesign/providers';
    
    const effectiveBaseUrl = input.baseUrl;
    let reasoning =
      input.reasoningLevel ??
      (inferReasoning(
        input.wire,
        input.model.modelId,
        effectiveBaseUrl,
        input.capabilities,
        input.model.provider,
      )
        ? reasoningForModel(input.model, effectiveBaseUrl)
        : undefined);

Summary

  • Review mode: follow-up after new commits. Found 2 issues: 4 unsigned non-merge commits still violate the repo's DCO requirement, and the legacy non-agent invocation path still ignores supportsReasoning=false when deciding whether to send the explicit reasoning knob. Not found in repo/docs in this checkout: docs/VISION.md, docs/PRINCIPLES.md.

Testing

  • Not run (automation): pnpm is not installed in this runner. Missing coverage for generate() / applyComment() with builtin openai or openrouter, a proxy baseUrl, and capabilities.supportsReasoning=false.

open-codesign Bot

...(input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {}),
...(input.wire !== undefined ? { wire: input.wire } : {}),
...(input.httpHeaders !== undefined ? { httpHeaders: input.httpHeaders } : {}),
...(input.capabilities !== undefined ? { capabilities: input.capabilities } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This now forwards capabilities into completeWithRetry(), but runModel() still derives the explicit reasoning option from reasoningForModel(input.model, input.baseUrl) above. For builtin openai / openrouter rows repointed at non-reasoning proxies, that means the request can still send reasoning: high|medium even though complete() now sets piModel.reasoning to false.

import { inferReasoning } from '@open-codesign/providers';

const effectiveBaseUrl = input.baseUrl;
let reasoning =
  input.reasoningLevel ??
  (inferReasoning(
    input.wire,
    input.model.modelId,
    effectiveBaseUrl,
    input.capabilities,
    input.model.provider,
  )
    ? reasoningForModel(input.model, effectiveBaseUrl)
    : undefined);

- Pass capabilities into buildPiModel so reasoning is inferred from
  ProviderCapabilities instead of hardcoded true.
- Gate thinkingLevel via inferReasoning so providers with explicit
  supportsReasoning=false do not get reasoning semantics sent under
  USE_AGENT_RUNTIME=true.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
@Sun-sunshine06
Sun-sunshine06 force-pushed the feat/connection-test-invocation-parity branch from efa4c67 to 65ce853 Compare April 24, 2026 15:37

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Major] runModel() still ignores capabilities.supportsReasoning=false when deciding whether to send the explicit reasoning option, so builtin openai / openrouter rows repointed at non-reasoning proxies can still pass connection tests and then fail on real generate() / applyComment() requests, evidence packages/core/src/index.ts:373, packages/core/src/generate.test.ts:150.
    Suggested fix:
    import { inferReasoning } from '@open-codesign/providers';
    
    const inferredReasoning =
      input.wire === undefined && input.capabilities === undefined
        ? true
        : inferReasoning(
            input.wire,
            input.model.modelId,
            input.baseUrl,
            input.capabilities,
            input.model.provider,
          );
    let reasoning =
      input.reasoningLevel ??
      (inferredReasoning ? reasoningForModel(input.model, input.baseUrl) : undefined);

Summary

  • Review mode: follow-up after new commits. Found 1 remaining issue: the non-agent generate() / applyComment() path still sends the explicit reasoning knob even after this PR started propagating capability profiles. Not found in repo/docs in this checkout: docs/VISION.md, docs/PRINCIPLES.md.

Testing

  • Not run (automation): pnpm is not installed in this runner. Suggested check after the fix: packages/core/src/generate.test.ts for the new proxy reasoning opt-out case.

open-codesign Bot

let reasoning =
input.reasoningLevel ??
(inferredReasoning ? reasoningForModel(input.model, input.baseUrl) : undefined);
// Self-healing: if the upstream rejects on reasoning mismatch, flip the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This now forwards capabilities into completeWithRetry(), but runModel() still derives the explicit reasoning option from reasoningForModel(input.model, input.baseUrl) above. For builtin openai / openrouter rows repointed at non-reasoning proxies, generate() / applyComment() can still send reasoning: high|medium and fail at invocation time even though the new capability plumbing and generate.test.ts expect reasoning to be omitted.

import { inferReasoning } from '@open-codesign/providers';

const inferredReasoning =
  input.wire === undefined && input.capabilities === undefined
    ? true
    : inferReasoning(
        input.wire,
        input.model.modelId,
        input.baseUrl,
        input.capabilities,
        input.model.provider,
      );
let reasoning =
  input.reasoningLevel ??
  (inferredReasoning ? reasoningForModel(input.model, input.baseUrl) : undefined);

…invocation-parity

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>

# Conflicts:
#	packages/providers/src/index.ts

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • None.

Summary

  • Review mode: follow-up after new commits. No additional diff-attributable issues found on the latest head. Residual risk: this runner does not include pnpm, so the updated provider-capability propagation and connection-probe coverage were reviewed statically only. Not found in repo/docs in this checkout: docs/VISION.md, docs/PRINCIPLES.md.

Testing

  • Not run (automation): pnpm is not installed in this runner.

open-codesign Bot

@Sun-sunshine06 Sun-sunshine06 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Findings

  1. [Major] Imported official OpenAI/OpenRouter providers without explicit supportsReasoning now lose reasoning heuristics

resolveProviderCapabilities() materializes every provider into Required<ProviderCapabilities>, so an imported openai-chat provider with no explicit capabilities.supportsReasoning becomes supportsReasoning: false. Then inferReasoning() treats that false as an opt-out unless providerId is exactly openai or openrouter.

That means a Codex/OpenCode imported provider like codex-openai / opencode-openai pointing at https://api.openai.com/v1 with model gpt-5.4 will now infer reasoning=false, whereas before this PR the official OpenAI base URL + gpt-5 heuristic enabled reasoning. Same shape applies to imported OpenRouter entries on https://openrouter.ai/api/v1.

Evidence:

  • packages/shared/src/config.ts:198
  • packages/shared/src/config.ts:224
  • packages/providers/src/wire-policy.ts:85
  • packages/providers/src/wire-policy.ts:89

Suggested fix: preserve the distinction between “explicit false” and “default false”. For example, pass raw/explicit capabilities into inferReasoning() for the reasoning decision, or carry an explicitCapabilities/capabilitySource flag alongside the resolved profile. Add a regression test for an imported official OpenAI provider with no supportsReasoning field and gpt-5.4, plus the existing explicit opt-out case.

  1. [Major] models:v1:list-for-provider still ignores modelDiscoveryMode / supportsModelsEndpoint

runProviderTest() now respects supportsModelsEndpoint:false by probing inference directly, but models:v1:list-for-provider still always builds and fetches the /models endpoint unless modelsHint exists. So a manual/inference-only provider can pass the new connection test and then still fail model listing with “Cannot reach provider /models endpoint”.

That keeps one of the #216 false-negative paths alive in the Settings/model-picker flow, and it also leaves the new modelDiscoveryMode only partially honored.

Evidence:

  • apps/desktop/src/main/connection-ipc.ts:937
  • apps/desktop/src/main/connection-ipc.ts:962
  • apps/desktop/src/main/connection-ipc.ts:967

Suggested fix: resolve capabilities in models:v1:list-for-provider and branch before fetching:

  • static-hint: return modelsHint
  • manual: return a structured response the renderer can treat as manual entry, or return the default model as a hint
  • models: fetch /models
    At minimum, if supportsModelsEndpoint === false, do not attempt /models.
  1. [Medium] Agent runtime retry path does not pass wire into classifyError()

completeWithRetry() now passes retryOpts.wire into classifyError(), which lets Anthropic gateway “messages API not implemented” errors avoid useless retries. The agent path uses withBackoff() directly but calls classifyError(err) without input.wire, so the same Anthropic-wire error will still retry in agent mode.

Evidence:

  • packages/core/src/agent.ts:925
  • packages/core/src/agent.ts:929
  • packages/providers/src/retry.ts:300

Suggested fix: call classifyError(err, input.wire) in the agent retry classifier and add a small regression test for anthropic-wire 5xx “not implemented” behavior.

Summary

Direction looks right for #216: provider config resolution, capabilities, runtime calls, and connection tests are much closer to one contract now. I would block on the first two because they affect real imported-provider behavior and the Settings flow. The third is smaller but worth fixing while this PR already touches retry parity.

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Minor] generateTitle() still drops explicitCapabilities — this new follow-up plumbs the field into the title IPC input, but the actual completeWithRetry() call still forwards only resolved capabilities, so imported OpenAI/OpenRouter rows on official bases keep the old reasoning-heuristic regression on title generation packages/core/src/index.ts:853
    Suggested fix:

    ...(input.capabilities !== undefined ? { capabilities: input.capabilities } : {}),
    ...(input.explicitCapabilities !== undefined
      ? { explicitCapabilities: input.explicitCapabilities }
      : {}),
  • [Minor] Legacy codesign:generate still misses the same propagation — the new v1 generate path now passes explicitCapabilities, but the compatibility shim kept for older renderers still calls runGenerate() without it, so that one-minor-release fallback keeps the imported-provider reasoning bug apps/desktop/src/main/index.ts:888
    Suggested fix:

    capabilities: active.capabilities,
    explicitCapabilities: active.explicitCapabilities,
    ...(allowKeyless ? { allowKeyless: true } : {}),

Summary

  • Review mode: follow-up after new commits. Two compatibility gaps remain: generateTitle() still drops explicitCapabilities, and the legacy codesign:generate shim still omits it. Not found in repo/docs in this checkout: docs/VISION.md, docs/PRINCIPLES.md.

Testing

  • Not run (automation): static review only. PR code/tests were not executed per the review security policy.

open-codesign Bot

...(input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {}),
...(input.wire !== undefined ? { wire: input.wire } : {}),
...(input.httpHeaders !== undefined ? { httpHeaders: input.httpHeaders } : {}),
...(input.capabilities !== undefined ? { capabilities: input.capabilities } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

explicitCapabilities is added to GenerateTitleInput, but this call still forwards only resolved capabilities. For imported OpenAI/OpenRouter rows on the official base URL, title generation therefore keeps the old reasoning-heuristic regression even though generate()/applyComment() were updated.

Suggested fix:

...(input.capabilities !== undefined ? { capabilities: input.capabilities } : {}),
...(input.explicitCapabilities !== undefined
  ? { explicitCapabilities: input.explicitCapabilities }
  : {}),

wire: active.wire,
...(active.httpHeaders !== undefined ? { httpHeaders: active.httpHeaders } : {}),
capabilities: active.capabilities,
explicitCapabilities: active.explicitCapabilities,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new path forwards explicitCapabilities, but the legacy codesign:generate shim below still omits it from runGenerate(...). Since that shim is intentionally kept for older renderers, those clients keep the imported-provider reasoning bug this PR is trying to close.

Suggested fix:

capabilities: active.capabilities,
explicitCapabilities: active.explicitCapabilities,
...(allowKeyless ? { allowKeyless: true } : {}),

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Major] explicitCapabilities still stops at the provider boundary — runModel() now infers reasoning from explicitCapabilities, but the downstream provider call still forwards only resolved capabilities, and complete() only reads opts.capabilities. For imported OpenAI/OpenRouter rows on official hosts, normal generate() / applyComment() requests therefore still build piModel.reasoning=false, which keeps pi-ai on the non-official reasoning path even though the explicit override intentionally omitted supportsReasoning packages/core/src/index.ts:371, packages/providers/src/index.ts:68, packages/providers/src/index.ts:266, packages/providers/src/index.ts:277
    Suggested fix:
    // packages/core/src/index.ts
    ...(input.explicitCapabilities !== undefined
      ? { explicitCapabilities: input.explicitCapabilities }
      : {}),
    
    // packages/providers/src/index.ts
    explicitCapabilities?: ProviderCapabilities;
    
    reasoning: inferReasoning(
      opts.wire,
      effectiveModelId,
      effectiveBaseUrl,
      opts.explicitCapabilities ?? opts.capabilities,
      model.provider,
    );

Summary

  • Review mode: follow-up after new commits. One provider-layer gap remains: the explicit-capability fix still does not reach normal generate() / applyComment() requests because complete() never sees explicitCapabilities. Not found in repo/docs in this checkout: docs/VISION.md, docs/PRINCIPLES.md.

Testing

  • Not run (automation)

open-codesign Bot

const effectiveBaseUrl = opts.baseUrl ?? piModel.baseUrl;
piModel = {
...piModel,
reasoning: inferReasoning(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

runModel() now distinguishes explicitCapabilities from resolved defaults, but the provider layer still only accepts/uses capabilities. For imported OpenAI/OpenRouter rows on official hosts, normal generate() / applyComment() calls therefore still build piModel.reasoning=false, so the explicit-override fix does not actually reach complete().

Suggested fix:

export interface GenerateOptions {
  // ...
  explicitCapabilities?: ProviderCapabilities;
}

reasoning: inferReasoning(
  opts.wire,
  effectiveModelId,
  effectiveBaseUrl,
  opts.explicitCapabilities ?? opts.capabilities,
  model.provider,
)

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings

  • [Major] runProviderTest() still returns a false positive when /models succeeds but the capability profile explicitly disables the wire's real inference endpoint. The new supportsChatCompletions / supportsResponsesApi checks only run after a /models 404 (or when /models is disabled), so Settings can still report "Connection OK" even though the first generation will fail on /chat/completions or /responses. Evidence: apps/desktop/src/main/connection-ipc.ts:566, apps/desktop/src/main/connection-ipc.test.ts:1208, apps/desktop/src/main/connection-ipc.test.ts:1251.
    Suggested fix:
    if (res.ok) {
      const unsupportedMessage =
        creds.wire === 'openai-chat' && creds.capabilities?.supportsChatCompletions === false
          ? 'Provider does not support Chat Completions API'
          : creds.wire === 'openai-responses' && creds.capabilities?.supportsResponsesApi === false
            ? 'Provider does not support Responses API'
            : null;
    
      if (unsupportedMessage !== null) {
        return {
          ok: false,
          code: 'NETWORK',
          message: unsupportedMessage,
          hint: 'Provider capability profile says this endpoint is not supported. Check wire selection.',
          diagnostics: makeConnectionDiagnostics(
            creds,
            'models',
            [url],
            inferenceUrl === null ? [] : [inferenceUrl],
          ),
        };
      }
    
      return { ok: true, probeMethod: 'models', diagnostics: ... };
    }

Summary

  • Review mode: follow-up after new commits. The explicitCapabilities propagation fix looks addressed, but one connection-test parity gap remains: a provider can still pass on GET /models even when its capability profile says the actual wire endpoint is unsupported.

Testing

  • Not run (automation): this runner has no installed workspace dependencies, and the checkout requires Node >=22 while the runner is on Node v20.20.2.
  • Suggested regression coverage: add runProviderTest() cases where /models returns 200 while supportsChatCompletions=false and where /models returns 200 while supportsResponsesApi=false.

open-codesign Bot

),
};
}
if (res.ok) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

supportsChatCompletions / supportsResponsesApi are only consulted after a /models 404 (or when /models is disabled). If /models returns 200 here, the connection test still reports success even when the capability profile explicitly says the real wire endpoint is unsupported, so Settings can say "Connection OK" while the first generation fails.

Suggested fix:

if (res.ok) {
  const unsupportedMessage =
    creds.wire === "openai-chat" && creds.capabilities?.supportsChatCompletions === false
      ? "Provider does not support Chat Completions API"
      : creds.wire === "openai-responses" && creds.capabilities?.supportsResponsesApi === false
        ? "Provider does not support Responses API"
        : null;

  if (unsupportedMessage !== null) {
    return {
      ok: false,
      code: "NETWORK",
      message: unsupportedMessage,
      hint: "Provider capability profile says this endpoint is not supported. Check wire selection.",
      diagnostics: makeConnectionDiagnostics(creds, "models", [url], inferenceUrl === null ? [] : [inferenceUrl]),
    };
  }
}

@Sun-sunshine06

Copy link
Copy Markdown
Collaborator Author

补充看了一下最新这条 review,我的结论是:这更像一个后续可优化的连接测试策略问题,而不是当前 PR 的阻塞项。

这次 PR 主要修的是 provider capability 与真实 invocation 之间的语义对齐问题,尤其是:

  • resolved capabilities 与 explicitCapabilities 的区分
  • imported official OpenAI/OpenRouter provider 的 reasoning heuristic 保留
  • runtime / legacy / title / provider boundary 的能力透传一致性

这些主链路问题现在已经补齐,并且对应的回归测试也已经覆盖,当前 CI 也是通过的。

你这次提到的点,范围已经缩到 runProviderTest() 的连接测试策略本身:

  • 当 supportsModelsEndpoint = true 且 GET /models 返回 200 时,当前实现会直接判定连接成功
  • 只有 /models 404 或显式禁用 /models 时,才会继续检查 supportsChatCompletions / supportsResponsesApi

所以这里的确还存在一个“诊断比真实调用更宽松”的边角场景,但它成立的前提是 capability profile 本身处于一种自相矛盾的组合,例如:

  • wire = openai-chat,但同时显式声明 supportsChatCompletions = false
  • wire = openai-responses,但同时显式声明 supportsResponsesApi = false
  • 并且 /models 仍然返回 200

这更像一个 follow-up 的连接测试收紧项,而不是当前 PR 新引入的 runtime/invocation regression。当前 PR 的核心问题已经解决,我认为这条不应继续阻塞合并。

如果后续要继续收紧 Settings 里的 connection parity,我们可以单独补一条 follow-up:
在 /models 成功时,也额外校验 capability profile 是否显式否定当前 wire endpoint。

@Sun-sunshine06
Sun-sunshine06 merged commit b93e0dc into main Apr 24, 2026
7 checks passed
@Sun-sunshine06
Sun-sunshine06 deleted the feat/connection-test-invocation-parity branch April 24, 2026 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:build Turbo/Vite/Biome/tsconfig toolchain area:core packages/core (generation orchestration) area:desktop apps/desktop (Electron shell, renderer) area:providers packages/providers (pi-ai adapter, model calls)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant