Embedder auto: try Expected Parrot first, with a bounded failover#78
Conversation
Correction to the prior fix, which dropped the Expected Parrot endpoint from the auto path entirely. The intent is to use EP first (it's the platform), then fall back to local — the real problem was only that an unavailable endpoint HUNG the gated validation for 600s. auto now: Expected Parrot endpoint first, behind a bounded one-text health probe on a daemon thread; if it doesn't respond within EP_PROBE_TIMEOUT_SECONDS (20s) or errors, fail over in seconds to a direct OpenAI key, then local sentence-transformers, then the built-in lexical embedder that always runs. So EP is preferred but can never stall the run. `--embedder edsl` still forces EP with no failover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR restores the Expected Parrot endpoint to
Confidence Score: 4/5Safe to merge — the core probe + failover logic is correct, tests cover the three key scenarios, and no existing behaviour is broken for non-auto embedder choices. The daemon-thread approach is sound and the join/is_alive check handles timeout correctly. The only rough edge is that the failover warning prints 'did not respond within 20s' even when the probe thread returned quickly with an exception (e.g., an invalid API key), which could mislead a user debugging auth failures into thinking the host is unreachable. zwill/twin_baseline_commands.py — specifically the warning message at lines 82-86 and the failure-reason path in _probe_embedder. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant U as Caller (--embedder auto)
participant R as resolve_baseline_embedder
participant P as _probe_embedder
participant D as Daemon Thread
participant EP as Expected Parrot API
U->>R: resolve_baseline_embedder(args, model)
alt EXPECTED_PARROT_API_KEY set
R->>P: "_probe_embedder(lambda: edsl_embedder, timeout=20s)"
P->>D: "Thread(target=_run, daemon=True).start()"
D->>EP: embedder(["health check"])
alt EP responds within 20s
EP-->>D: vectors
D-->>P: result["vectors"] set
P-->>R: embedder (success)
R-->>U: EP embedder
else timeout or error
EP-->>D: error / hangs
D-->>P: result["error"] set (or thread still alive)
P-->>R: None
R->>R: print warning to stderr
R-->>U: openai / sentence-transformers / hashing fallback
end
else No EP key
R-->>U: openai / sentence-transformers / hashing fallback
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant U as Caller (--embedder auto)
participant R as resolve_baseline_embedder
participant P as _probe_embedder
participant D as Daemon Thread
participant EP as Expected Parrot API
U->>R: resolve_baseline_embedder(args, model)
alt EXPECTED_PARROT_API_KEY set
R->>P: "_probe_embedder(lambda: edsl_embedder, timeout=20s)"
P->>D: "Thread(target=_run, daemon=True).start()"
D->>EP: embedder(["health check"])
alt EP responds within 20s
EP-->>D: vectors
D-->>P: result["vectors"] set
P-->>R: embedder (success)
R-->>U: EP embedder
else timeout or error
EP-->>D: error / hangs
D-->>P: result["error"] set (or thread still alive)
P-->>R: None
R->>R: print warning to stderr
R-->>U: openai / sentence-transformers / hashing fallback
end
else No EP key
R-->>U: openai / sentence-transformers / hashing fallback
end
Reviews (1): Last reviewed commit: "Embedder auto: try Expected Parrot first..." | Re-trigger Greptile |
| print( | ||
| "warning: the Expected Parrot embeddings endpoint did not respond within " | ||
| f"{EP_PROBE_TIMEOUT_SECONDS:.0f}s; falling back to a local embedder for the conditional baseline.", | ||
| file=sys.stderr, | ||
| ) |
There was a problem hiding this comment.
The warning message always says "did not respond within Xs" regardless of whether the probe failed due to a timeout or due to an exception (e.g., auth error, 500, connection refused). When
_probe_embedder catches an exception in _run, the thread finishes quickly — it doesn't time out — yet the user still sees "did not respond within 20s", which implies an unreachable host rather than a bad key or server error. This makes it harder to diagnose misconfigured credentials vs. genuine network issues.
| print( | |
| "warning: the Expected Parrot embeddings endpoint did not respond within " | |
| f"{EP_PROBE_TIMEOUT_SECONDS:.0f}s; falling back to a local embedder for the conditional baseline.", | |
| file=sys.stderr, | |
| ) | |
| print( | |
| "warning: the Expected Parrot embeddings endpoint did not respond successfully within " | |
| f"{EP_PROBE_TIMEOUT_SECONDS:.0f}s (timed out or returned an error); " | |
| "falling back to a local embedder for the conditional baseline.", | |
| file=sys.stderr, | |
| ) |
Corrects #77, which dropped the Expected Parrot endpoint from
--embedder autoentirely. That was wrong — the intent is to use EP first (it's the platform), then fall back to local. The real problem was only that an unavailable endpoint hung the gated validation for 600s.Now
autotries the Expected Parrot embeddings endpoint first, behind a bounded one-text health probe on a daemon thread. If it responds, use it. If it doesn't respond withinEP_PROBE_TIMEOUT_SECONDS(20s) or errors, fail over in seconds to: a directOPENAI_API_KEY→ local sentence-transformers → the built-in lexical embedder (always runs).So EP is preferred, but it can never stall the run. The daemon probe thread means a hung endpoint never blocks process exit either.
--embedder edslstill forces EP with no failover.Testing
pytest— 261 passed (resolver test: healthy EP is chosen first; a dead EP fails over to local with a warning; no keys + no local → the lexical embedder, never an error/hang).ruffclean.🤖 Generated with Claude Code