Skip to content

review: #424 + follow-ups - #427

Open
rejojer wants to merge 3 commits into
pre-424-mainfrom
fix/client-config-followups
Open

review: #424 + follow-ups#427
rejojer wants to merge 3 commits into
pre-424-mainfrom
fix/client-config-followups

Conversation

@rejojer

@rejojer rejojer commented Aug 26, 2026

Copy link
Copy Markdown
Member

Review view only — do not merge. Base is pre-424-main, pinned at 416e304 (main right before #424 landed), so the diff is everything #424 shipped plus the follow-ups on top, the way #400 pins pre-389-main.

Follow-ups since v0.2.11:

  • 5e2dc9b.env search ends at the cwd tree (find_dotenv returns '', and or None handed dotenv its own walk up from utils.py); a local client with a blanked chat_model refuses at the chat door instead of AttributeError; storage_path typed str | os.PathLike[str] to match _ARG_TYPES now that py.typed ships.
  • 4e9c56cindex= / chat= typed Mapping[str, Any] so the exported config shapes pass; a comment and two docstrings stop overclaiming.
  • 90d6289 — test renamed for what it asserts.

The mergeable PR for the same commits targets main separately.

https://claude.ai/code/session_017Fd7jVm366S2Xamzhxv6yb

… home (#424)

* feat: the client grows two sides — documents and chat each pick their home

One client, two independent switches: api_key decides where documents
live (the PageIndex cloud, or the local store); a configured chat model
decides who answers (your own model in your process, or the managed
cloud chat). Their free combination opens the bridge — cloud documents,
your model — and the fourth cell stays unspellable.

- index=/chat= slots: string shorthand or grouped dict, 1:1 with the
  flat arguments; one spelling per side, sides mix freely
- optional "type" everywhere (top-level and in either dict): always
  omittable, checked against the content, meaningful alone —
  type="cloud" is a keyless cloud spelling
- PAGEINDEX_API_KEY is read only when the code explicitly says cloud
  (PageIndexCloudClient(), type="cloud", "pageindex-cloud",
  {"type": "cloud"}); a bare PageIndexClient() stays local
- bare mode words ("cloud", "local", …) are reserved: they error
  with the real spellings instead of silently parsing as model names
- bridge chat runs the in-process agent over the live cloud MCP tools
  and instructions; doc_id targets at the prompt level; citations stay
  managed-only; an auth-shaped backend failure explains whose
  credentials run the model
- typed shapes (IndexConfig, ChatConfig) ship as optional annotations

Every previously working program is byte-for-byte unchanged: the only
behavioral deltas are error paths — reworded guidance, and the
api_key+chat_model combination graduating from an error into the
bridge.

* fix: the constructor refuses empty and mistyped values on every spelling

- .env keys reach all four keyless-cloud spellings: utils' import-time
  load_dotenv now runs before every PAGEINDEX_API_KEY read
- an empty chat-side value ("", {}) errors instead of silently selecting
  own-model chat on the default model; None-valued slot keys mean absent,
  exactly like the flat arguments
- _local_chat derives from chat_model, so a post-construction assignment
  switches the whole client, never half of it
- model= beside a slot gets the split guidance (index_model=/chat_model=)
  instead of "two spellings of the same thing"
- the messages door wraps provider failures through _model_backend_error,
  and 401s count as auth-shaped even without "api key" in the text
- keyless-cloud hints name the spelling that actually combines; slot
  strings are stripped; wrong-typed values raise PageIndexAPIError
- retrieve_model/chat_backend docs drop the stale "Local mode only";
  the local-scope refusal no longer claims bridge tools are server-scoped

* fix: type= cross-checks the index slot; the cloud pinned class frees its chat side

- type= beside index= now does what the docstring promises: agreement
  passes, disagreement errors, and a mistyped value reports the
  vocabulary error instead of a spelling collision
- PageIndexCloudClient grows the chat-side arguments (chat=, chat_model,
  retrieve_model, chat_backend), so "pin the index side" is literally
  true and the chat surfaces' construct-with-chat_model guidance is
  followable on it
- the four chat doors' doc_id entries carry the enforcement split the
  config helpers already state (local: tool-layer allowlist; cloud:
  prompt-level / server-side)
- types.py stops claiming slot keys share the flat names — the side
  prefix is factored out, index={"model"} is index_model=

* docs: bridge-reachable wording — dependency errors say own-model chat, hints name a chat= model

- the three framework-missing errors said "in local mode", which is
  wrong on a bridge client (cloud documents + own model) — they now
  explain the dependency the way the surfaces do: your own chat model
- the construct-with guidance reads "(or a chat= model)": a bare
  chat="pageindex-cloud" is also chat= but selects the managed side
- the mechanical Local-only → Own-model-chat-only substitution left
  orphan fragments and two overlong lines; those paragraphs re-flowed

* fix: managed chat reads None; the bridge stops paying per-turn tool lists

- a managed-chat cloud client stores chat_model/chat_backend as None, so
  the documented attribute reads instead of raising AttributeError;
  _local_chat derives from "is a chat model configured"
- McpBridge caches tools/list per session — every chat turn rebuilds the
  tool set, and the round trip was pure latency; the 404 session-expiry
  reset drops the cache with the session
- run_messages builds tools before the transport: on a bridge client
  that build is network I/O, and a failure there stranded a per-call
  anthropic client ahead of the try/finally

* refactor: the side declaration is spelled mode=, not type=

"type" is Python's own word — a builtin, and "data type" beside the
TypedDict shapes; "mode" is what the SDK already calls the two sides
("local mode", "cloud mode"). Same grammar everywhere the declaration
appears: the top-level argument, the index dict, the chat dict, the
typed shapes. The rename also frees the builtin inside the constructor,
so the shape-check error names the offending class through type() again.
"type" in a slot dict is now an ordinary unknown key.

* fix: the reserved-word errors stop calling "cloud" not a mode word

With the declaration key spelled mode=, 'index="cloud" is not a mode
word' contradicted its own remedy, index={"mode": "cloud"} — "cloud" is
exactly a mode value. The four bare strings are reserved words; the
message now says so.

* fix: a blank tools/list is not cached; the auth note's managed exit is chat-lane only

- McpBridge.list_tools caches only a non-empty list — a transient blank
  (a deploy blip, a gate misconfiguration) would otherwise run every later
  turn with zero tools while the instructions still name them, and only a
  404 session reset could clear it
- the 401 architecture note appends "drop the chat model configuration"
  only on the chat lane: responses() and messages() refuse a client
  without an own model, so on those lanes the exit sent the caller in a
  circle
- CloudIndexConfig says api_key is omittable only while mode: "cloud"
  stays — index={} refuses as an empty dict rather than reading the env

* fix: the bridge fetches tools/list per call again; .env resolves from the cwd

- McpBridge.list_tools no longer caches: the tool set is built once per
  SDK call (Agent(tools=...) ahead of Runner.run; build_anthropic_tools
  ahead of tool_runner), not per model turn, so the cache saved one round
  trip per later call while a mid-pagination 404 replayed a dead cursor
  into a duplicated (and cached) list, and the list went out by reference
  across a lock dropped between miss and store
- utils.load_dotenv searches upward from the cwd: a bare load_dotenv()
  walked up from utils.py, which is site-packages for an installed SDK,
  so the four keyless-cloud spellings never saw a project-root .env; the
  package-relative walk stays as the fallback
- the emptiness guard strips strings: chat_model=" " selected own-model
  chat, the silent flip the guard's own comment rules out
- the _local_chat comment stops advertising post-construction assignment
  as a full mode switch

* fix: the pinned classes take index=/chat=; "cloud"/"local" are mode words; a blank chat_model stays managed

- PageIndexLocalClient takes index= and chat=, PageIndexCloudClient takes
  index= — the grouped spelling of the flat vocabulary each already took;
  their refusals name the class and an exit that class can take, and the
  mode cross-check runs before any environment read
- "cloud" and "local" are accepted wherever "pageindex-cloud" was (index=,
  chat=, mode=, {"mode": ...}), case- and whitespace-insensitive; "hosted"
  and "managed" still refuse, pointing at the real word
- every spelling strips its strings, and the slot spellings' type/empty
  errors name the slot key (index["model"]), not the flat argument
- _local_chat treats a blank chat_model as managed: the constructor
  refuses "", so assignment agrees instead of opening the bridge on a
  nameless model; openai_agent_config carries no model then either
- an empty MCP tools/list raises like empty instructions does — a
  zero-tool agent would answer from the model's own knowledge silently
- enable_citations names the real gate (managed vs own chat), not
  "cloud-only", on a cloud own-model client
- pageindex/py.typed: the exported config TypedDicts reach installed
  type-checked callers

* test: the two framework-door tests skip without openai-agents

as_openai_tools() and openai_agent_config() need the agents package, which
the "without frameworks" CI legs do not install — the same importorskip
every other test on those doors already carries.
…k chat_model refuses at the chat door; storage_path is typed PathLike (#428)

* fix: .env stays unset when the cwd tree has none; a local client with a blank chat_model refuses at the chat door; storage_path is typed PathLike

find_dotenv(usecwd=True) returns '' when nothing is reachable from the
cwd, and `or None` turned that into load_dotenv's own upward walk from
utils.py — the install-dir leak the cwd search was added to replace. A
pip-installed SDK could load another project's .env from above
site-packages, silently.

_local_chat treats a blank chat_model as "managed chat", which a client
without an api_key does not have: chat_completions() then reached for
LocalAPI.chat_completions and raised a bare AttributeError. The managed
branch now refuses as a PageIndexAPIError naming chat_model.

py.typed made the annotations authoritative while storage_path was typed
str; _ARG_TYPES accepts os.PathLike, so Path(...) ran fine and failed the
user's type check. Both signatures and LocalIndexConfig now say so.

Claude-Session: https://claude.ai/code/session_017Fd7jVm366S2Xamzhxv6yb

* fix: the exported config shapes pass into index=/chat=; a comment and two docstrings stop overclaiming

The slots were annotated dict[str, Any]. A TypedDict is consistent with
Mapping[str, object], never with dict (PEP 589: a dict-typed receiver
could write arbitrary keys through it), so the four shapes types.py
exports — and py.typed advertises to installed callers' checkers —
could not be passed to the one place they describe. pyright on a probe
that does exactly that: 9 errors before, 0 after. The constructor only
reads the slot (items(), then a fresh conf dict), so Mapping is the
honest bound; a plain dict is a Mapping, and TypedDict instances are
plain dicts at runtime, so nothing moves at runtime.

The _ARG_TYPES comment said "every value" is shape-checked; api_key is
not in the table (its empty check is separate, its type check stays
unchecked by ruling), so the comment now speaks for the table only.

_local_doc_scope and _require_local_scope still explained the cloud
drop as "scoping is server-side" — true of the managed chat, which
never reaches either function. What reaches them on a cloud client is
own-model chat and the config helpers, whose cloud tools take no
allowlist: targeting there is prompt-level only, as the error message
between them already said.

434 passed; pyright on pageindex/ unchanged at 235 (0 in the touched
files, before and after).

Claude-Session: https://claude.ai/code/session_01TxG8u8x29XRnK4yscZVCch

* test: the install-dir .env test is named for what it asserts

Claude-Session: https://claude.ai/code/session_017Fd7jVm366S2Xamzhxv6yb
@rejojer
rejojer force-pushed the fix/client-config-followups branch from 90d6289 to b9a9a3b Compare August 26, 2026 07:09
@rejojer

rejojer commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

…alling cloud tool scoping server-side (#429)

fix: the slots accept any Mapping at runtime, as their annotation admits; eight docstrings stop calling cloud tool scoping server-side

4e9c56c widened index=/chat= to Mapping[str, Any] so the exported
TypedDicts pass a checker, but _resolve_index_slot/_resolve_chat_slot
still dispatched on isinstance(..., dict): a MappingProxyType or ChainMap
was pyright-clean and raised "must be a string or a dict" at construction.
The resolvers now narrow on Mapping — the comprehension already copies,
so a read-only proxy proves the caller's mapping is never mutated.

4e9c56c corrected three of eleven "scoping is server-side" sites; the
remaining eight said the same untrue thing about doc_id on cloud (its
tools carry no allowlist — targeting is prompt-level, as the runtime
error already explains). Deleted rather than reworded.

local_chat.py's module docstring predates own-model chat over the cloud
bridge; storage_path's prose now names the PathLike 5e2dc9b typed.

Claude-Session: https://claude.ai/code/session_01VQ6mruXZBgw9Hjii8KPbQP
@rejojer
rejojer force-pushed the fix/client-config-followups branch from 2e606b8 to 174f95f Compare August 26, 2026 09:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant