Skip to content

Commit e4d082d

Browse files
authored
feat: add reasoning_rewrite adapter and debounced inputs (#446)
## Summary of Changes This Pull Request introduces the **`reasoning_rewrite` Provider Adapter** and a highly reusable **`DebouncedInput`** UI component to solve two distinct operational and performance issues in Plexus. ### 1. The `reasoning_rewrite` Adapter (Backend) Different LLM backends expect reasoning configurations via various custom keys (e.g., `enable_thinking`, `budget_tokens`, or `thinking.type`). * **Final State:** The backend now includes a declarative `reasoning_rewrite` adapter that maps unified reasoning properties (`reasoning.enabled`, `reasoning.effort`) to provider-specific layouts in outbound request payloads, and then strips the unified values afterward if configured. * **Safety & Correctness:** * Uses `structuredClone(payload)` upon first mutation to deeply clone request bodies. This prevents any in-place mutation of nested objects in the original payload, keeping request-retries and downstream logging 100% stable. * Extensively tested with 718 custom test scenarios (including DeepSeek-R1 sequential stripping and mapping scenarios). ### 2. High-Performance `DebouncedInput` Shared UI (Frontend) To solve rendering overhead when typing in heavy model-level or provider-level editor forms: * **Final State:** Added a reusable `DebouncedInput` component under `packages/frontend/src/components/ui/DebouncedInput.tsx`. It wraps the standard Input, holding local text state and propagating updates via a 300ms debounce. * **Performance Upgrades Applied:** * **`reasoning_rewrite` Adapter:** Source, when conditions, and target rewrites now use debounced inputs. * **`model_override` Adapter:** Rewrite target and trigger condition key-value fields now use debounced inputs. * **Per-Model "Extra Body Fields":** Key-value inputs are now debounced. * **Provider-Level Stall Detection Overrides:** Replaced five duplicate manual state draft hooks and custom blur logic with `DebouncedInput`. * **Provider-Level Headers & "Extra Body Fields":** Key-value inputs are now debounced. All type checking, OpenAPI schemas, biome formatting, and test suites are 100% green and verified.
2 parents 389ab1b + ec056bb commit e4d082d

12 files changed

Lines changed: 1969 additions & 136 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@ plexus
8080

8181
# Profiling output
8282
.prof/
83+
.grepai/

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/openapi/components/schemas/ProviderConfig.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,17 @@ properties:
228228
Used for providers that expose reasoning variants as separate model
229229
names rather than respecting reasoning-related request fields.
230230
231+
- `reasoning_rewrite` — Declaratively rewrites reasoning/thinking fields
232+
in outbound request payloads. Different providers accept reasoning
233+
effort via different field names (e.g. `enable_thinking`, `thinking.type`,
234+
`chat_template_kwargs.enable_thinking`, `budget_tokens`, `thinking_budget`).
235+
This adapter maps from unified fields (e.g. `reasoning.enabled`,
236+
`reasoning.effort`) to provider-specific formats. Accepts a `rules`
237+
array in options where each rule specifies a `source` dotted path,
238+
an optional `when` condition (with `op` and `value`), an array of
239+
`rewrites` (each with `target` and `value`), and optional `strip`
240+
paths to remove after rewriting.
241+
231242
Pass-through optimisation is automatically disabled when any adapter
232243
is active.
233244
timeoutMs:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"agent-browser": "^0.27.0",
88
"bun-types": "1.3.14",
99
"octokit": "^5.0.5",
10+
"typescript-language-server": "^5.2.0",
1011
"vitest": "^4.1.6",
1112
"yaml": "^2.9.0"
1213
},

packages/backend/src/config.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,59 @@ export type ModelOverrideRule = z.infer<typeof ModelOverrideRuleSchema>;
117117
export type ModelOverrideOptions = z.infer<typeof ModelOverrideOptionsSchema>;
118118
export type AdapterEntry = z.infer<typeof AdapterEntrySchema>;
119119

120+
// ─── Reasoning Rewrite Adapter Config ────────────────────────────────
121+
122+
const ValueTransformSchema = z.union([
123+
z.object({ from: z.literal('source') }),
124+
z.object({ from: z.literal('map'), values: z.record(z.string(), z.any()) }),
125+
z.object({ from: z.literal('boolean'), truthy: z.any(), falsy: z.any() }),
126+
]);
127+
128+
const FieldRewriteSchema = z.object({
129+
/** Dotted path to write (e.g. "enable_thinking", "thinking.type"). */
130+
target: z.string().min(1),
131+
/**
132+
* Value to write at the target path.
133+
* Literal primitives are written as-is.
134+
* Objects with { from: "source" | "map" | "boolean" } trigger transforms.
135+
*/
136+
value: z.any(),
137+
});
138+
139+
const MatchConditionSchema = z.object({
140+
/** Comparison operator. */
141+
op: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'present', 'absent']),
142+
/** Value to compare against (for eq/neq/gt/gte/lt/lte). */
143+
value: z.any().optional(),
144+
/** Values array for "in" operator. */
145+
values: z.array(z.any()).optional(),
146+
});
147+
148+
const ReasoningRewriteRuleSchema = z.object({
149+
/** Dotted path to read from the payload (e.g. "reasoning.enabled", "reasoning.effort"). */
150+
source: z.string().min(1),
151+
/** Optional condition on the source value. Omit = match any (presence check). */
152+
when: MatchConditionSchema.optional(),
153+
/** Rewrites to apply when the source matches. All matching rewrites apply. */
154+
rewrites: z.array(FieldRewriteSchema).min(1),
155+
/**
156+
* Dotted paths to REMOVE from the payload after rewrites are applied.
157+
* Use to strip unified fields the provider doesn't understand
158+
* (e.g. "reasoning" when mapping to "enable_thinking" instead).
159+
*/
160+
strip: z.array(z.string().min(1)).optional(),
161+
});
162+
163+
const ReasoningRewriteOptionsSchema = z.object({
164+
rules: z.array(ReasoningRewriteRuleSchema).min(1),
165+
});
166+
167+
export type ValueTransform = z.infer<typeof ValueTransformSchema>;
168+
export type FieldRewrite = z.infer<typeof FieldRewriteSchema>;
169+
export type MatchCondition = z.infer<typeof MatchConditionSchema>;
170+
export type ReasoningRewriteRule = z.infer<typeof ReasoningRewriteRuleSchema>;
171+
export type ReasoningRewriteOptions = z.infer<typeof ReasoningRewriteOptionsSchema>;
172+
120173
const ModelProviderConfigSchema = z.object({
121174
pricing: PricingSchema.default({
122175
source: 'simple',

0 commit comments

Comments
 (0)