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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant