sync: update from private repo (91d8d79c)
CI / build-and-test (push) Failing after 7m0s

This commit is contained in:
oss-sync
2026-07-09 23:57:26 +00:00
parent 63d34d7cf6
commit 2044f0a2c4
81 changed files with 3456 additions and 372 deletions
+205 -3
View File
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { HelpText } from './HelpText';
import { EnvOverrideWarning, FieldLabel, FieldInput } from './formUtils';
@@ -27,6 +27,22 @@ interface LlmWorker {
/** llama.cpp の prompt 評価進捗(return_progress)を要求。llama.cpp 系専用のオプトイン。 */
returnProgress?: boolean;
healthcheckIntervalSeconds?: number;
/**
* OpenAI 互換 request body へ浅いマージする任意 JSON(例:
* `{"reasoning_effort":"max"}`)。予約キー(model/messages/stream/
* stream_options/tools/tool_choice/temperature)はクライアント側で
* 無視される。空欄はフィールド自体を省略(`{}` ではなく undefined)。
*/
extraBody?: Record<string, unknown>;
/** Phase 1 で消費される、このワーカーが対応する reasoning effort の宣言リスト。 */
reasoningEfforts?: string[];
/**
* effort をリクエストボディへ注入する形。body=トップレベル
* reasoning_effortvLLM 向け)、chat_template_kwargs=
* chat_template_kwargs.reasoning_effortllama-server 向け)。
* 未指定は body 扱い。
*/
reasoningEffortMode?: 'body' | 'chat_template_kwargs';
/**
* Phase 1 compat: older `provider.workers[].proxy: true` rows are
* mapped to `connectionType: aao_gateway` by the normalizer. We
@@ -80,6 +96,100 @@ function detectSelfLoop(endpoint: string | undefined): boolean {
return false;
}
/**
* JSON textarea for `worker.extraBody`. The raw text the operator is
* mid-typing lives only in this component's state (same local-draft idea as
* `SecretInput`), and `onChange` (which writes into the config draft that
* `Save & Apply` persists) is called ONLY when the text parses as a JSON
* object. Invalid JSON — including valid-but-non-object JSON like an array,
* string, or number — therefore never reaches the draft.
*
* Because the invalid draft is withheld from the config draft, a save
* triggered by ANOTHER dirty field would silently drop it — so the field
* also reports its validity upward via `onValidityChange(fieldKey, valid)`
* (SectionFormProps contract): ConfigForm disables Save & Apply while any
* key is invalid. The flag is cleared on unmount (row removed, section
* switched) so a stale key can never permanently brick Save.
*
* If the `value` prop changes to something we did not emit (Discard Changes,
* row moved/removed above this one), the local draft re-syncs from the prop
* and any error is cleared.
*
* An emptied textarea reports `undefined` (field absent) rather than `{}`.
*/
function ExtraBodyField({ value, onChange, fieldKey, onValidityChange }: {
value: Record<string, unknown> | undefined;
onChange: (v: Record<string, unknown> | undefined) => void;
/** Stable identity for the validity flag, e.g. `llm.workers.0.extraBody`. */
fieldKey: string;
onValidityChange?: (fieldKey: string, valid: boolean) => void;
}) {
const { t } = useTranslation('settings');
const [text, setText] = useState(() => (value === undefined ? '' : JSON.stringify(value, null, 2)));
const [error, setError] = useState<string | null>(null);
// Last value WE pushed via onChange. If the prop diverges from it, the
// change came from outside (discard / row shift) → re-sync the draft.
const lastEmitted = useRef(value);
useEffect(() => {
if (value !== lastEmitted.current) {
lastEmitted.current = value;
setText(value === undefined ? '' : JSON.stringify(value, null, 2));
setError(null);
}
}, [value]);
// Report validity upward; clear the flag on unmount or key change so a
// removed row / switched section never leaves Save permanently disabled.
useEffect(() => {
onValidityChange?.(fieldKey, error === null);
return () => onValidityChange?.(fieldKey, true);
}, [fieldKey, error, onValidityChange]);
const handleChange = (raw: string) => {
setText(raw);
if (raw.trim() === '') {
setError(null);
lastEmitted.current = undefined;
onChange(undefined);
return;
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
setError(t('llmWorkers.extraBodyInvalid'));
return;
}
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
setError(t('llmWorkers.extraBodyInvalid'));
return;
}
setError(null);
const obj = parsed as Record<string, unknown>;
lastEmitted.current = obj;
onChange(obj);
};
return (
<div className="col-span-2">
<FieldLabel>extra_body</FieldLabel>
<textarea
aria-label="extra_body"
value={text}
onChange={e => handleChange(e.target.value)}
rows={4}
placeholder={'{"reasoning_effort": "high"}'}
className={`w-full px-2.5 py-2 text-[13px] font-mono border rounded-md focus:ring-2 focus:ring-accent-ring outline-none bg-canvas ${
error ? 'border-red-400 focus:border-red-400' : 'border-hairline focus:border-accent'
}`}
/>
{error && <p className="text-2xs text-red-600 mt-1">{error}</p>}
<HelpText>{t('llmWorkers.extraBodyHelp')}</HelpText>
</div>
);
}
/**
* Settings → LLM → Workers.
*
@@ -103,18 +213,48 @@ function detectSelfLoop(endpoint: string | undefined): boolean {
* - `aao_gateway` rows show a heuristic self-loop warning when the
* endpoint host looks like the current AAO instance
*/
export function LlmWorkersForm({ config, onChange, overriddenByEnv }: SectionFormProps) {
export function LlmWorkersForm({ config, onChange, overriddenByEnv, onValidityChange, resetToken }: SectionFormProps) {
const { t } = useTranslation('settings');
const llm: LlmConfigShape = config.llm ?? {};
const workers: LlmWorker[] = Array.isArray(llm.workers) ? llm.workers : [];
const retry = llm.retry ?? {};
// Stable per-row identity, independent of array position. `w.id` is a
// user-editable free-text field (can be empty/duplicated) so it isn't
// usable as a React key; array index breaks the moment a row above the
// one you're mid-editing is removed/reordered, because React then
// reuses that DOM/component instance for a *different* worker — the
// in-progress ExtraBodyField draft/validity (keyed by index) silently
// migrates to the wrong row. `uidsRef` mirrors the `workers` array
// 1:1 by position; addWorker/removeWorker/moveWorker mutate it in the
// same lockstep as their `onChange('llm.workers', ...)` call so a row's
// uid follows it across reorders and survives removal of *other* rows.
//
// The top-of-render check below only handles the array drifting out
// from under us for reasons other than the three handlers below (e.g.
// Discard Changes reverting a structural edit made in another tab, or
// the initial mount): if the lengths disagree, we don't know which
// positions correspond to which old rows, so we just regenerate fresh
// uids for the current shape. Any in-progress ExtraBodyField draft on
// screen at that moment loses its identity and unmounts/remounts —
// acceptable, since unsaved invalid JSON was never going to reach
// config.yaml anyway, and the unmount fires ConfigForm's validity
// cleanup so Save can't stay wedged.
const uidCounterRef = useRef(0);
const uidsRef = useRef<string[]>([]);
const nextUid = () => `w-${uidCounterRef.current++}`;
if (uidsRef.current.length !== workers.length) {
uidsRef.current = workers.map(() => nextUid());
}
const uids = uidsRef.current;
const updateWorker = (index: number, patch: Partial<LlmWorker>) => {
const next = workers.map((w, i) => (i === index ? { ...w, ...patch } : w));
onChange('llm.workers', next);
};
const removeWorker = (index: number) => {
uidsRef.current = uidsRef.current.filter((_, i) => i !== index);
onChange('llm.workers', workers.filter((_, i) => i !== index));
};
@@ -124,6 +264,12 @@ export function LlmWorkersForm({ config, onChange, overriddenByEnv }: SectionFor
const next = [...workers];
const [removed] = next.splice(index, 1);
next.splice(target, 0, removed);
const nextUids = [...uidsRef.current];
const [removedUid] = nextUids.splice(index, 1);
nextUids.splice(target, 0, removedUid);
uidsRef.current = nextUids;
onChange('llm.workers', next);
};
@@ -136,6 +282,7 @@ export function LlmWorkersForm({ config, onChange, overriddenByEnv }: SectionFor
maxConcurrency: 1,
roles: [],
};
uidsRef.current = [...uidsRef.current, nextUid()];
onChange('llm.workers', [...workers, next]);
};
@@ -165,12 +312,22 @@ export function LlmWorkersForm({ config, onChange, overriddenByEnv }: SectionFor
)}
{workers.map((w, i) => {
const uid = uids[i];
const isGateway = w.connectionType === 'aao_gateway' || w.proxy === true;
const showSelfLoop = isGateway && selfLoopFlags[i];
const endpointOverridden = i === 0 && overriddenByEnv['llm.workers[0].endpoint'];
const modelOverridden = i === 0 && overriddenByEnv['llm.workers[0].model'];
return (
<div key={i} className="border border-slate-200 rounded-lg p-4 space-y-3 relative">
// resetToken is folded into the key so Discard Changes / a
// fresh config load-refetch force a full remount of every row.
// That resyncs ExtraBodyField's local textarea+error draft from
// the reverted `value` prop even when the prop is unchanged
// from what the field itself last emitted (the case its own
// value-diff re-sync guard can't detect on its own) — see
// SectionFormProps.resetToken. ConfigForm clears invalidKeys
// directly in the same state update, so this remount only
// needs to reconcile the visuals, not the validity Set.
<div key={`${uid}:${resetToken ?? 0}`} className="border border-slate-200 rounded-lg p-4 space-y-3 relative">
<div className="absolute top-2 right-2 flex gap-1">
<button
onClick={() => moveWorker(i, -1)}
@@ -334,6 +491,51 @@ export function LlmWorkersForm({ config, onChange, overriddenByEnv }: SectionFor
{t('llmWorkers.returnProgress')}
</label>
</div>
<div className="col-span-2 border-t border-slate-100 pt-3 mt-1">
<p className="text-2xs font-medium text-slate-500 mb-2">{t('llmWorkers.advancedTitle')}</p>
</div>
<div className="col-span-2">
<FieldLabel>reasoning_efforts</FieldLabel>
<FieldInput
aria-label="reasoning_efforts"
value={Array.isArray(w.reasoningEfforts) ? w.reasoningEfforts.join(', ') : ''}
onChange={v => {
const efforts = v.split(',').map(s => s.trim()).filter(s => s.length > 0);
updateWorker(i, { reasoningEfforts: efforts.length > 0 ? efforts : undefined });
}}
placeholder="high, medium, low"
/>
<HelpText>{t('llmWorkers.reasoningEffortsHelp')}</HelpText>
</div>
<div>
<FieldLabel>reasoning_effort_mode</FieldLabel>
<select
aria-label="reasoning_effort_mode"
value={w.reasoningEffortMode ?? ''}
onChange={e => {
const next = e.target.value;
updateWorker(i, {
reasoningEffortMode: next === '' ? undefined : (next as 'body' | 'chat_template_kwargs'),
});
}}
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-canvas"
>
<option value="">{t('llmWorkers.reasoningEffortModeDefault')}</option>
<option value="body">body</option>
<option value="chat_template_kwargs">chat_template_kwargs</option>
</select>
<HelpText>{t('llmWorkers.reasoningEffortModeHelp')}</HelpText>
</div>
<ExtraBodyField
value={w.extraBody}
onChange={extraBody => updateWorker(i, { extraBody })}
fieldKey={`llm.workers.${uid}.extraBody`}
onValidityChange={onValidityChange}
/>
</div>
</div>
);