Skip to content

Embedder auto: try Expected Parrot first, with a bounded failover#78

Merged
johnjosephhorton merged 1 commit into
mainfrom
embedder-ep-first-with-failover
Jul 10, 2026
Merged

Embedder auto: try Expected Parrot first, with a bounded failover#78
johnjosephhorton merged 1 commit into
mainfrom
embedder-ep-first-with-failover

Conversation

@johnjosephhorton

Copy link
Copy Markdown
Contributor

Corrects #77, which dropped the Expected Parrot endpoint from --embedder auto entirely. 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

auto tries 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 within EP_PROBE_TIMEOUT_SECONDS (20s) or errors, fail over in seconds to: a direct OPENAI_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 edsl still 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).
  • ruff clean.

🤖 Generated with Claude Code

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>
@johnjosephhorton johnjosephhorton merged commit 32642bd into main Jul 10, 2026
1 of 3 checks passed
@johnjosephhorton johnjosephhorton deleted the embedder-ep-first-with-failover branch July 10, 2026 11:28
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR restores the Expected Parrot endpoint to --embedder auto (accidentally dropped in #77) while preventing it from hanging the process when unavailable. A bounded daemon-thread health probe (EP_PROBE_TIMEOUT_SECONDS = 20s) gates the EP embedder before committing to it, so a slow or unreachable endpoint fails over in seconds rather than stalling for 600s.

  • _probe_embedder() runs a one-text embed on a daemon thread and joins with a timeout; None is returned on timeout, exception, or empty result — the daemon thread can never block process exit.
  • resolve_baseline_embedder() now prefers EP when EXPECTED_PARROT_API_KEY is set and the probe succeeds, then falls back through OpenAI → sentence-transformers → hashing, unchanged.
  • Tests cover healthy EP (chosen first), dead EP (fast failover + warning), and no-key/no-local (lexical fallback).

Confidence Score: 4/5

Safe 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

Filename Overview
zwill/twin_baseline_commands.py Core logic change: adds _probe_embedder() with a daemon thread + bounded join, and rewires resolve_baseline_embedder() to try EP first under that probe. Thread-safety is correct (join() provides the memory barrier; timeout path never reads result). Minor issue: the warning message conflates timeout and error failure modes.
tests/test_twin_baseline.py Test renamed and updated to cover healthy EP (probe succeeds → EP chosen), dead EP (probe errors → local failover + warning), and no-key/no-local (lexical fallback). All three new scenarios are well-exercised.
zwill/cli_parser.py Help text for --embedder updated in both subcommand parsers to reflect the new EP-first auto behaviour; no logic changes.
zwill/guides/agent-workflow.md Documentation updated to describe EP-first auto ordering and the bounded health-probe failover; accurately reflects the new implementation.

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
Loading
%%{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
Loading

Reviews (1): Last reviewed commit: "Embedder auto: try Expected Parrot first..." | Re-trigger Greptile

Comment on lines +82 to +86
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,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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,
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant