Skip to content

feat: OpenRouter provider routing and reasoning control#1958

Open
chiefmojo wants to merge 6 commits into
MemTensor:mainfrom
chiefmojo:feat/openrouter-routing
Open

feat: OpenRouter provider routing and reasoning control#1958
chiefmojo wants to merge 6 commits into
MemTensor:mainfrom
chiefmojo:feat/openrouter-routing

Conversation

@chiefmojo

@chiefmojo chiefmojo commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Problem

When using OpenRouter as the LLM provider, operators have no way to:

  1. Exclude specific upstream providers from serving requests (provider.ignore)
  2. Set a preferred provider order (provider.order)
  3. Control reasoning effort per model slot (reasoning)

All three are standard OpenRouter-specific request fields that MemOS currently passes through to the HTTP body without any config surface.

Changes

Config schema — adds providerIgnore, providerOrder, and reasoning to LlmSchema and SkillEvolverSchema:

llm:
  provider: openai_compatible
  endpoint: https://openrouter.ai/api/v1
  apiKey: sk-...
  providerIgnore:
    - together
    - deepinfra
  providerOrder:
    - anthropic
    - google
  reasoning:
    enabled: true
    max_tokens: 8000

SkillEvolverSchema:
  provider: openai_compatible
  endpoint: https://openrouter.ai/api/v1
  providerIgnore:
    - novita

Provider layeropenai.ts detects OpenRouter endpoints and injects the provider routing object; reasoning is forwarded unconditionally when set. Non-OpenRouter endpoints are unaffected.

EmbeddingproviderIgnore/providerOrder added to the embedding config schema; openai.ts embedding provider applies the same routing logic.

PipelinebootstrapMemoryCore forwards the new fields to createLlmClient for the skillEvolver slot.

Tests

  • providers.test.ts — reasoning forwarding, routing injection for non/streaming calls, non-OpenRouter passthrough
  • bootstrap-llm-config.test.ts — verifies routing fields propagate to dedicated LLM clients
  • load.test.ts — defaults to empty arrays, accepts explicit values

@Memtensor-AI
Memtensor-AI changed the base branch from dev-20260604-v2.0.19 to dev-v2.0.22 July 1, 2026 13:15
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

Automated Test Results: PASSED

Cloud test-engine rerun against dev-v2.0.22 completed successfully.

  • Run: tr-013a21fe-6ec on cloud test-engine 10011
  • memos_local_openclaw/unit: 10 passed, 0 failed, 0 skipped
  • memos_local_plugin/unit: 90 passed, 0 failed, 0 skipped

Manual code review is still required before merge.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

Conflict status update: I verified locally that refs/pull/1958/head cleanly merges with dev-v2.0.22, and the previous cloud test-engine run passed (tr-013a21fe-6ec, 100/100). However, pushing the merge commit back to the PR head failed because the head fork repository is not accessible (Repository not found).

Current conclusion: automated tests passed, but the PR cannot be merged while GitHub still reports it as conflicting. The branch needs to be updated by the author/maintainer, or a replacement branch/PR needs to be created from the locally clean merge commit and then retested.

@CarltonXiang
CarltonXiang deleted the branch MemTensor:main July 3, 2026 07:25
@syzsunshine219 syzsunshine219 reopened this Jul 3, 2026
@syzsunshine219
syzsunshine219 changed the base branch from dev-v2.0.22 to main July 3, 2026 08:21
@Memtensor-AI Memtensor-AI added the area:plugin OpenClaw & Hermes label Jul 8, 2026
@Memtensor-AI
Memtensor-AI requested a review from bittergreen July 8, 2026 11:43
@Memtensor-AI Memtensor-AI added the area:model llm + embedder + reranker label Jul 9, 2026
@Memtensor-AI Memtensor-AI removed the area:model llm + embedder + reranker label Jul 9, 2026
@syzsunshine219
syzsunshine219 changed the base branch from main to dev-v2.0.23 July 9, 2026 06:23
@syzsunshine219
syzsunshine219 changed the base branch from dev-v2.0.23 to main July 9, 2026 06:32

@shinetata shinetata left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this PR, Erick — OpenRouter provider routing is a useful gap to close, and the test coverage (streaming / non-streaming / embedding, non-OpenRouter passthrough, config defaults, bootstrap forwarding) is solid. The Cloud test-engine rerun also passed (100/100) on dev-v2.0.22.

Before we can merge, a few items need attention. Marking as changes_requested to track them.

Blocking — merge conflict

GitHub currently reports this PR as CONFLICTING / DIRTY against main. The head last merged main on 2026-07-04; main has since advanced to 13fbd437 (2026-07-09). The earlier bot note referenced dev-v2.0.22, but that branch no longer exists, so a fresh rebase onto the current main is needed.

Could you rebase feat/openrouter-routing onto main and force-push? If you'd prefer a maintainer to take it over, just say so and we'll open a replacement PR (we've done this for a couple of other stale external PRs).

Code — three improvements

1. DRY: the routing helper is duplicated 4×

applyOpenRouterProviderRouting is implemented nearly verbatim in:

  • apps/memos-local-plugin/core/llm/providers/openai.ts
  • apps/memos-local-plugin/core/embedding/providers/openai.ts
  • apps/memos-local-openclaw/src/shared/llm-call.ts
  • apps/memos-local-openclaw/src/ingest/providers/openai.ts (here it's still inlined in buildRequestBody, not even extracted)

Please factor it into a single shared helper per app (the two apps/ don't share code, so one helper under each app's shared layer is fine) and have all call sites use it. The inlined version in ingest/providers/openai.ts should call the helper for consistency.

2. OpenRouter detection is brittle

endpoint.includes("openrouter.ai") is case-sensitive and substring-based:

  • OpenRouter.AI / OPENROUTER.ai won't match.
  • A self-hosted proxy whose URL happens to contain openrouter.ai as a substring (path / query / hostname fragment) would be misclassified.
  • Conversely, users fronting OpenRouter via their own domain (reverse proxy / CNAME) get no routing even though the upstream is OpenRouter.

Suggest normalizing with new URL(endpoint).hostname lowercased and comparing against a known host list, and/or adding an explicit openrouter: true opt-in flag on the LLM / embedding config so routing isn't inferred from the URL at all.

3. reasoning is forwarded unconditionally

In core/llm/providers/openai.ts:

if (config.reasoning) body.reasoning = config.reasoning;

This sends reasoning to every endpoint, not just OpenRouter. Non-OpenRouter providers may reject the unknown field or silently ignore it. Either gate it behind the OpenRouter check, or document explicitly that it's a passthrough intended for OpenAI-compatible reasoning models and accept that behavior.

Deadline

Could you push an update by 2026-07-23? If there's no activity by then, to keep main moving we'll close this PR and open a maintainer-side replacement with the above items addressed (credit will be preserved in the replacement PR description). Happy to help with the rebase or the refactor if useful — just ping here.

@chiefmojo
chiefmojo force-pushed the feat/openrouter-routing branch from 40af2fd to 2c8178d Compare July 17, 2026 03:56
@Memtensor-AI Memtensor-AI added the status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 label Jul 17, 2026
@Memtensor-AI
Memtensor-AI requested review from hijzy and whipser030 July 17, 2026 03:56
@Memtensor-AI

Memtensor-AI commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #1958
Task: b5be9075636df774
Base: main
Head: feat/openrouter-routing

🔍 OpenCodeReview found 13 issue(s) in this PR.

⚠️ 1 warning(s) occurred during review.


1. apps/memos-local-openclaw/src/shared/openrouter.ts (L3-L9)

Duplicate code across two packages: apps/memos-local-openclaw/src/shared/openrouter.ts and apps/memos-local-plugin/core/openrouter.ts are byte-for-byte identical in logic and interface. Any future change (e.g., adding a new OpenRouter host or a new routing field) must be applied in both places, creating a maintenance burden and a risk of the two copies drifting out of sync.

Consider extracting this into a shared workspace package (e.g., packages/openrouter-routing) that both memos-local-openclaw and memos-local-plugin can depend on, following the existing pattern seen in apps/openwork-memos-integration/packages/shared. If a shared package is not feasible due to publishing constraints, at least add a comment here (and in the plugin copy) explicitly linking the two files so maintainers know to keep them in sync.

💡 Suggested Change

Before:

export interface OpenRouterRoutingConfig {
  endpoint?: string;
  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter?: boolean;
  providerIgnore?: string[];
  providerOrder?: string[];
}

After:

// NOTE: This file is intentionally duplicated in
// apps/memos-local-plugin/core/openrouter.ts because the two
// packages are published independently. Keep both files in sync.
export interface OpenRouterRoutingConfig {
  endpoint?: string;
  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter?: boolean;
  providerIgnore?: string[];
  providerOrder?: string[];
}

2. apps/memos-local-openclaw/src/shared/openrouter.ts (L22-L23)

Silent no-op when isOpenRouter is true but no routing fields are set: When the endpoint matches OpenRouter (or openRouter: true) but neither providerIgnore nor providerOrder is provided, the function returns true without adding any provider key to the body. Callers (e.g., in llm-call.ts) rely on the true return value to enable additional OpenRouter-specific fields such as reasoning. This means those extra fields will be applied even though the provider routing object was intentionally left out — which is likely correct, but the asymmetry between "provider block not injected" and "return value signals OpenRouter is active" is non-obvious and could confuse future readers.

Consider adding a short comment to document this intentional behaviour.

💡 Suggested Change

Before:

  if (Object.keys(provider).length > 0) body.provider = provider;
  return true;

After:

  // Only inject the `provider` object when at least one routing field is
  // specified; the true return value still signals OpenRouter is active so
  // callers can enable other OpenRouter-specific features (e.g. reasoning).
  if (Object.keys(provider).length > 0) body.provider = provider;
  return true;

3. apps/memos-local-plugin/core/embedding/providers/openai.ts (L45)

The return value of applyOpenRouterProviderRouting (a boolean indicating whether OpenRouter routing was applied) is silently discarded. While harmless today, callers that never check the return value make the boolean API surface misleading. If the return value is not needed here (and in similar call sites in the LLM provider), consider changing the function signature to void, or using void explicitly to signal intentional discard:

void applyOpenRouterProviderRouting(config, body);

Alternatively, keep the boolean but add a log when routing is applied, which would be useful for debugging.


4. apps/memos-local-plugin/core/embedding/types.ts (L34-L35)

Type mismatch: openRouter is boolean (required) here but boolean | undefined (optional) in the schema and the shared OpenRouterRoutingConfig interface.

In EmbeddingSchema (schema.ts), openRouter is declared with Type.Optional(Bool(false)), making its resolved TypeScript type boolean | undefined. The shared utility OpenRouterRoutingConfig in openrouter.ts also types it as openRouter?: boolean. However, the EmbeddingConfig interface here declares it as a required boolean, creating a mismatch.

Consequences:

  1. EmbeddingConfig cannot be used directly where OpenRouterRoutingConfig is expected without a type error (or silent narrowing), because one considers the field always-present and the other doesn't.
  2. Code that checks if (config.openRouter) in openrouter.ts already safely handles undefined, so the required boolean in this interface is unnecessarily strict and inconsistent.

Make openRouter optional (openRouter?: boolean) to align with the schema, the defaults object, and OpenRouterRoutingConfig.

💡 Suggested Change

Before:

  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter: boolean;

After:

  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter?: boolean;

5. apps/memos-local-plugin/core/config/schema.ts (L61-L69)

The effort union includes "minimal" and "xhigh" which are not part of the OpenRouter reasoning API. The documented values are "low", "medium", "high", and "none". Using unsupported values will silently be passed through to the API and may result in unexpected behaviour or API errors at runtime.

Consider removing the non-standard literals, or at minimum adding a comment to document that these are speculative/forward-looking extensions.

💡 Suggested Change

Before:

  effort: Type.Optional(Type.Union([
    Type.Literal("minimal"),
    Type.Literal("none"),
    Type.Literal("low"),
    Type.Literal("medium"),
    Type.Literal("high"),
    Type.Literal("xhigh"),
    Type.Literal("max"),
  ])),

After:

  effort: Type.Optional(Type.Union([
    Type.Literal("none"),
    Type.Literal("low"),
    Type.Literal("medium"),
    Type.Literal("high"),
  ])),

6. apps/memos-local-plugin/core/config/schema.ts (L43-L48)

The reasoning field is added to both LlmSchema and SkillEvolverSchema, but the EmbeddingSchema also received the OpenRouter routing fields (providerIgnore, providerOrder, openRouter). However, OpenRouter's documentation positions reasoning controls as LLM/chat-completion-specific. More importantly, embedding providers generally do not use a reasoning feature, but the routing fields here are also inconsistent: the defaults.ts adds providerIgnore: [] / providerOrder: [] / openRouter: false for embedding, yet these defaults are not reflected in the schema with Type.Optional — they use Type.Optional in the schema (no default at the field level), while defaults.ts provides non-optional defaults as [] and false. This is inconsistent with how LlmSchema and SkillEvolverSchema fields are typed (all Type.Optional) — the TypeBox Static<> type for these optional fields will be string[] | undefined, while defaults.ts always sets them to string[]. This mismatch could cause subtle type errors downstream when consuming the config.

💡 Suggested Change

Before:

  providerIgnore: Type.Optional(Type.Array(Type.String(), { default: [] })),
  /** OpenRouter provider routing — preferred order. */
  providerOrder: Type.Optional(Type.Array(Type.String(), { default: [] })),
  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter: Type.Optional(Bool(false)),
  cache: Type.Object({

After:

  providerIgnore: Type.Array(Type.String(), { default: [] }),
  /** OpenRouter provider routing — preferred order. */
  providerOrder: Type.Array(Type.String(), { default: [] }),
  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter: Bool(false),
  cache: Type.Object({

7. apps/memos-local-plugin/core/config/schema.ts (L43-L45)

The { default: [] } option is placed on the inner Type.Array(...) element rather than on the wrapping Type.Optional(...). In TypeBox, { default } is meaningful at the schema node level for JSON Schema generation and validation coercion. Since the outer Type.Optional wraps without a default, the inner default on Type.Array will be ignored by most validators/coercers when the field is absent. The default should be moved to the Type.Optional wrapper, or the field should be non-optional with a default on Type.Array directly (without Type.Optional).

💡 Suggested Change

Before:

  providerIgnore: Type.Optional(Type.Array(Type.String(), { default: [] })),
  /** OpenRouter provider routing — preferred order. */
  providerOrder: Type.Optional(Type.Array(Type.String(), { default: [] })),

After:

  providerIgnore: Type.Optional(Type.Array(Type.String()), { default: [] }),
  /** OpenRouter provider routing — preferred order. */
  providerOrder: Type.Optional(Type.Array(Type.String()), { default: [] }),

8. apps/memos-local-plugin/core/llm/providers/openai.ts (L78-L81)

The 3-line reasoning injection block is duplicated verbatim in both complete() and stream(). Since this logic is non-trivial (a conditional call + conditional property assignment), it should be extracted into a shared helper to avoid divergence. If the serialization logic or the condition ever needs to change, it must be updated in two places.

Consider a small utility such as:

function applyOpenRouterReasoning(config: LlmConfig, body: Record<string, unknown>): void {
  if (applyOpenRouterProviderRouting(config, body)) {
    const reasoning = config.reasoning && serializeOpenRouterReasoning(config.reasoning);
    if (reasoning) body.reasoning = reasoning;
  }
}

and call applyOpenRouterReasoning(config, body) in both methods.

💡 Suggested Change

Before:

if (applyOpenRouterProviderRouting(config, body)) {
      const reasoning = config.reasoning && serializeOpenRouterReasoning(config.reasoning);
      if (reasoning) body.reasoning = reasoning;
    }

After:

    applyOpenRouterReasoning(config, body);

    const headers: Record<string, string> = {};
    if (config.apiKey) {
      headers.Authorization = `Bearer ${config.apiKey}`;
    }
    Object.assign(headers, config.headers);

    const { json, durationMs } = await httpPostJson

9. apps/memos-local-plugin/core/llm/types.ts (L39-L40)

Type mismatch: openRouter is declared as required boolean here, but in schema.ts it is Type.Optional(Bool(false)), meaning the inferred ResolvedConfig.llm.openRouter type is boolean | undefined. Any code path that spreads or assigns ResolvedConfig.llm directly into an LlmConfig without an explicit ?? false coercion will hit a TypeScript compile error (or silently pass undefined at runtime if the coercion is missing).

Consider making this field optional (openRouter?: boolean) to align with the schema, and let consumers default it with config.openRouter ?? false at the point of use — consistent with how providerIgnore and providerOrder are typed as string[] | undefined.

💡 Suggested Change

Before:

  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter: boolean;

After:

  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter?: boolean;

10. apps/memos-local-plugin/core/embedding/types.ts (L35)

Type mismatch: openRouter is declared as a required boolean in EmbeddingConfig, but it is typed as optional (boolean | undefined) in two other places:

  1. The schema definition: Type.Optional(Bool(false)) in EmbeddingSchema (schema.ts) — the resolved TypeScript type is boolean | undefined.
  2. The shared utility interface: openRouter?: boolean in OpenRouterRoutingConfig (openrouter.ts).

This inconsistency forces any code constructing an EmbeddingConfig to always explicitly supply openRouter, while the schema and the utility both treat it as optional. Change openRouter: boolean to openRouter?: boolean to align with both the schema and OpenRouterRoutingConfig.

💡 Suggested Change

Before:

  openRouter: boolean;

After:

  openRouter?: boolean;

11. apps/memos-local-plugin/core/openrouter.ts (L23-L26)

The applyOpenRouterProviderRouting function has a mixed responsibility: it both mutates the body object (side-effect) and returns a boolean to indicate whether OpenRouter is active. At the embedding call site the return value is intentionally ignored — only the mutation matters. This dual-purpose design is fragile because callers that forget to check the boolean will silently still have body.provider injected, while callers that check the boolean but don't realise body was already mutated may act inconsistently.

Consider splitting concerns: always mutate body when the routing fields are non-empty (regardless of the return value), or extract the boolean check into a separate exported isOpenRouterConfig() predicate so callers can decide independently whether to apply extra fields like reasoning.

💡 Suggested Change

Before:

export function applyOpenRouterProviderRouting(
  config: OpenRouterRoutingConfig,
  body: Record<string, unknown>,
): boolean {

After:

/** Returns true when the target endpoint is OpenRouter (or is explicitly flagged). */
export function isOpenRouterConfig(config: OpenRouterRoutingConfig): boolean {
  return isOpenRouter(config);
}

/**
 * Injects OpenRouter provider-routing fields into `body` when applicable.
 * Always call this unconditionally; use `isOpenRouterConfig` separately
 * to gate OpenRouter-specific fields like `reasoning`.
 */
export function applyOpenRouterProviderRouting(
  config: OpenRouterRoutingConfig,
  body: Record<string, unknown>,
): void {

12. apps/memos-local-plugin/core/openrouter.ts (L11)

The OPENROUTER_HOSTS set is defined with a single hard-coded hostname ("openrouter.ai"). While safe for now, this string is the only "business rule" that determines routing behaviour. If OpenRouter ever changes its domain or adds a secondary hostname, this set must be updated manually and the location is not obvious. Consider adding a brief inline comment documenting why specific hosts are listed here and how to extend the list.


13. apps/memos-local-plugin/core/pipeline/memory-core.ts (L124-L135)

Duplicate type risk: DedicatedLlmConfig is a manually maintained copy of the shape already defined in SkillEvolverSchema / ResolvedConfig["skillEvolver"]. Now that new fields (providerIgnore, providerOrder, openRouter, reasoning) are being added to the schema and this local type in the same PR, future schema additions will require a second manual edit here, and the types will silently diverge if one is forgotten.

Consider deriving the type directly from ResolvedConfig to make this a zero-maintenance alias:

// Add the import if not already present
import type { ResolvedConfig } from "../config/schema.js";

// Replace the local struct with a derived alias
type DedicatedLlmConfig = ResolvedConfig["skillEvolver"];

Both skillEvolver and l3Llm use the same SkillEvolverSchema, so a single alias covers both casts.

💡 Suggested Change

Before:

type DedicatedLlmConfig = {
  provider?: string;
  model?: string;
  endpoint?: string;
  apiKey?: string;
  temperature?: number;
  timeoutMs?: number;
  providerIgnore?: string[];
  providerOrder?: string[];
  openRouter?: boolean;
  reasoning?: ReasoningConfig;
};

After:

type DedicatedLlmConfig = ResolvedConfig["skillEvolver"];

🧹 Filtered 7 low-confidence OCR finding(s) before posting/fix-loop (duplicate: 7).

Generated by cloud-assistant via Open Code Review.

@chiefmojo

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and force-pushed 2c8178d. Consolidated OpenRouter routing into one helper per app, replaced all four call sites, added robust hostname detection plus explicit openRouter proxy opt-in, and gated/normalized reasoning fields. Also fixed dedicated L3 config propagation. Validation: 66 focused plugin tests, 3 OpenClaw call tests, plugin type-check, and make format.

@chiefmojo
chiefmojo requested a review from shinetata July 17, 2026 04:12
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (134/134 executed). memos_local_openclaw/unit: 33/33, memos_local_plugin/unit: 101/101. Duration: 17s [advisory, non-gating] AI-generated tests on branch test/auto-gen-650698fd619b8f1a-20260717122622: 110/112 passed, 2 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: feat/openrouter-routing

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 17, 2026
@whipser030

Copy link
Copy Markdown
Collaborator

Thanks for the update — the earlier concerns around OpenRouter routing detection, reasoning passthrough for non-OpenRouter requests, and helper reuse appear to be addressed well, and the test coverage looks solid.

Before merging, could we please clarify two small edge cases?

  1. reasoning: {} may currently enable reasoning through the default value of enabled. This seems slightly at odds with the documentation stating that omitted configuration should preserve the provider/model default. Would it make sense to avoid enabling it by default here?

  2. In a few dedicated LLM configuration paths, openRouter may be passed as undefined while its type is declared as a required boolean. It may be worth adding ?? false at the handoff point, or making the field optional consistently, to keep the runtime behavior and type contract aligned.

Also, when convenient, could you please confirm that the latest branch now merges cleanly with current main? Once that is confirmed, I’d be happy to take another look.

@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Jul 17, 2026
@chiefmojo

Copy link
Copy Markdown
Contributor Author

@whipser030 Addressed in aa75c36:

  • Empty reasoning blocks no longer default enabled to true and are omitted from OpenRouter request bodies, preserving provider/model defaults.
  • Dedicated skillEvolver and l3Llm clients now normalize an absent openRouter flag to false.

Added focused regressions for both paths. The branch contains current main and GitHub reports the PR mergeable.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (137/137 executed). memos_local_openclaw/unit: 33/33, memos_local_plugin/unit: 104/104. Duration: 17s [advisory, non-gating] AI-generated tests on branch test/auto-gen-b5be9075636df774-20260717203542: 126/126 passed — these do NOT affect the PR verdict; review the branch manually.

Branch: feat/openrouter-routing

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:plugin OpenClaw & Hermes status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants