Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,19 @@ is longer: the computed backoff or the server hint).
from pollux import Options

options = Options(
system_instruction="You are a concise analyst.", # Optional global behavior guide
response_schema=MyPydanticModel, # Structured output extraction
reasoning_effort="medium", # Reserved for future provider support
delivery_mode="realtime", # "deferred" reserved for v1.1+
delivery_mode="realtime", # "deferred" reserved for future provider batch APIs
)
```

See [Sources and Patterns](sources-and-patterns.md#structured-output) for
a complete structured output example.

Conversation options are provider-dependent in v1.1: OpenAI supports
`history`/`continue_from`; Gemini remains unsupported.

## Safety Notes

- `Config` is immutable (`frozen=True`). Create a new instance to change values.
Expand Down
1 change: 1 addition & 0 deletions docs/overrides/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
.md-footer {
margin-left: 0 !important;
margin-right: 0 !important;
max-width: none !important;
padding-inline: 0;
}
</style>
Expand Down
18 changes: 9 additions & 9 deletions docs/reference/provider-capabilities.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Provider Capabilities

This page defines the v1.0 capability contract by provider.
This page defines the v1.1 capability contract by provider.

Pollux is **capability-transparent**, not capability-equalizing: providers are allowed to differ, and those differences are surfaced clearly.

## v1.0 Policy
## v1.1 Policy

- Provider feature parity is **not** required for release.
- Unsupported features must fail fast with clear errors.
- New provider features do not require immediate cross-provider implementation.

## Capability Matrix (v1.0)
## Capability Matrix (v1.1)

| Capability | Gemini | OpenAI | Notes |
|---|---|---|---|
Expand All @@ -19,20 +19,20 @@ Pollux is **capability-transparent**, not capability-equalizing: providers are a
| Local file inputs | ✅ | ✅ | OpenAI uses Files API upload |
| PDF URL inputs | ✅ (via URI part) | ✅ (native `input_file.file_url`) | |
| Image URL inputs | ✅ (via URI part) | ✅ (native `input_image.image_url`) | |
| YouTube URL inputs | ✅ | ⚠️ limited | OpenAI parity layer (download/re-upload) is out of scope for v1.0 |
| YouTube URL inputs | ✅ | ⚠️ limited | OpenAI parity layer (download/re-upload) is out of scope for v1.1 |
| Provider-side context caching | ✅ | ❌ | OpenAI provider returns unsupported for caching |
| Structured outputs (`response_schema`) | ✅ | ✅ | JSON-schema path in both providers |
| Reasoning controls (`reasoning_effort`) | ❌ | ❌ | Reserved for future provider enablement |
| Deferred delivery (`delivery_mode="deferred"`) | ❌ | ❌ | Explicitly disabled in v1.0 |
| Conversation continuity (`history`, `continue_from`) | ❌ | | Reserved/disabled in v1.0 |
| Deferred delivery (`delivery_mode="deferred"`) | ❌ | ❌ | Explicitly disabled in v1.1 |
| Conversation continuity (`history`, `continue_from`) | ❌ | | OpenAI-native continuation; single prompt per call |

## Important OpenAI Notes

- Pollux uploads local files with:
- `purpose="user_data"`
- finite `expires_after` metadata
- Automatic file deletion is not part of v1.0 yet.
- Remote URL support in v1.0 is intentionally narrow and explicit:
- Automatic file deletion is not part of v1.1 yet.
- Remote URL support in v1.1 is intentionally narrow and explicit:
- PDFs
- images

Expand All @@ -48,7 +48,7 @@ from pollux import Config
config = Config(
provider="openai",
model="gpt-5-nano",
enable_caching=True, # not supported for OpenAI in v1.0
enable_caching=True, # not supported for OpenAI in v1.1
)
# At execution time, Pollux raises:
# ConfigurationError: Provider does not support caching
Expand Down
8 changes: 4 additions & 4 deletions docs/sources-and-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ Example of a complete envelope:
}
```

## v1.0 Notes
## v1.1 Notes

- Conversation continuity (`history`, `continue_from`) is reserved and
disabled in v1.0.
- `delivery_mode="deferred"` is reserved and disabled in v1.0.
- Conversation continuity (`history`, `continue_from`) is currently
OpenAI-only and supports one prompt per call.
- `delivery_mode="deferred"` remains reserved and disabled.
- Provider feature support varies. See
[Provider Capabilities](reference/provider-capabilities.md).
17 changes: 16 additions & 1 deletion docs/stylesheets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
--pollux-sidebar-bottom-space: 1.25rem;
--pollux-nav-gutter: 0.25rem;
--pollux-nav-indent: 0.45rem;
--pollux-nav-link-margin-top: 0.5em;
--pollux-nav-link-margin-top: 0.35em;
--pollux-toc-link-size: 0.72rem;
--pollux-toc-link-line-height: 1.45;
--pollux-toc-link-padding-y: 0.18rem;
Expand Down Expand Up @@ -122,6 +122,7 @@ body {
/* ── Navigation links ────────────────────────────────────────── */

.md-nav__item--section > .md-nav__link {
font-size: 0.7rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
Expand Down Expand Up @@ -288,6 +289,17 @@ body {
.md-nav--primary .md-nav__link {
margin-top: var(--pollux-nav-link-margin-top);
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.md-nav__item--section {
margin-top: 0.75em;
}

.md-nav__item--section:first-child {
margin-top: 0;
}

/* Boosted-specificity active link: .md-nav--primary prefix matches
Expand Down Expand Up @@ -341,8 +353,11 @@ body {
font-size: var(--pollux-toc-link-size);
line-height: var(--pollux-toc-link-line-height);
margin-top: 0;
overflow: hidden;
padding-block: var(--pollux-toc-link-padding-y);
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
}

.md-nav--secondary .md-nav__link--active::before {
Expand Down
10 changes: 6 additions & 4 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ Use this order — most failures resolve by step 2.
provider. Re-run a minimal prompt after fixing any mismatch.

3. **Unsupported feature** — Compare your options against
[Provider Capabilities](reference/provider-capabilities.md). In v1.0,
`delivery_mode="deferred"`, `history`, and `continue_from` are reserved.
[Provider Capabilities](reference/provider-capabilities.md).
`delivery_mode="deferred"` is reserved; conversation continuity is
provider-dependent (OpenAI-only in v1.1).

4. **Source and payload** — Reduce to one source + one prompt and retry.
For OpenAI remote URLs in v1.0, only PDF and image URLs are supported.
Expand All @@ -61,12 +62,13 @@ Or pass `api_key` directly in `Config(...)`.

**Fix:** verify the model belongs to the selected provider.

## Option Not Implemented in v1.0
## Option Not Implemented Yet

**Symptom:** `ConfigurationError` mentioning `delivery_mode="deferred"`,
`history`, or `continue_from`.

These are intentionally reserved and disabled in v1.0.
`delivery_mode="deferred"` is intentionally reserved.
`history`/`continue_from` require a provider with conversation support.

## `status == "partial"`

Expand Down
69 changes: 43 additions & 26 deletions src/pollux/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import asyncio
from dataclasses import dataclass, field
import logging
import os
from pathlib import Path
import time
from typing import TYPE_CHECKING, Any
Expand Down Expand Up @@ -59,6 +58,7 @@ class ExecutionTrace:
cache_name: str | None = None
duration_s: float = 0.0
usage: dict[str, int] = field(default_factory=dict)
conversation_state: dict[str, Any] | None = None


async def execute_plan(
Expand All @@ -81,24 +81,12 @@ async def execute_plan(
wants_conversation = (
options.history is not None or options.continue_from is not None
)
if wants_conversation:
enabled = os.environ.get(
"POLLUX_EXPERIMENTAL_CONVERSATION", ""
).strip().lower() in {"1", "true", "yes", "on"}
if not enabled:
raise ConfigurationError(
"Conversation options are reserved for a future release",
hint=(
"Remove history/continue_from for now, or set "
"POLLUX_EXPERIMENTAL_CONVERSATION=1 to opt in during development."
),
)

if options.delivery_mode == "deferred":
provider_name = type(provider).__name__
raise ConfigurationError(
f"delivery_mode='deferred' is not implemented yet for provider {provider_name}",
hint="Use delivery_mode='realtime' for v1.0.",
hint="Use delivery_mode='realtime' for now.",
)
if options.response_schema is not None and not caps.structured_outputs:
raise ConfigurationError(
Expand All @@ -115,6 +103,11 @@ async def execute_plan(
"Provider does not support conversation continuity",
hint="Remove history/continue_from or choose a provider with conversation support.",
)
if wants_conversation and len(prompts) != 1:
raise ConfigurationError(
"Conversation continuity currently supports exactly one prompt per call",
hint="Use run() or run_many() with a single prompt when passing history/continue_from.",
)

if (not provider.supports_uploads) and any(
isinstance(p, dict)
Expand All @@ -130,6 +123,10 @@ async def execute_plan(
schema = options.response_schema_json()

history = options.history
conversation_history: list[dict[str, str]] = []
if history is not None:
conversation_history = [dict(item) for item in history]

previous_response_id: str | None = None
if options.continue_from is not None:
state = options.continue_from.get("_conversation_state")
Expand All @@ -142,16 +139,18 @@ async def execute_plan(
),
)

state_history = state.get("history")
if history is None and isinstance(state_history, list):
conversation_history = [
item
for item in state_history
if isinstance(item, dict)
and isinstance(item.get("role"), str)
and isinstance(item.get("content"), str)
]

if history is None:
state_history = state.get("history")
if isinstance(state_history, list):
history = [
item
for item in state_history
if isinstance(item, dict)
and isinstance(item.get("role"), str)
and isinstance(item.get("content"), str)
]
history = conversation_history

prev = state.get("response_id")
previous_response_id = prev if isinstance(prev, str) else None
Expand Down Expand Up @@ -185,7 +184,7 @@ async def execute_plan(
key=plan.cache_key,
model=config.model,
parts=shared_parts, # Use resolved parts with URIs
system_instruction=None,
system_instruction=options.system_instruction,
ttl_seconds=config.ttl_seconds,
retry_policy=retry_policy,
)
Expand Down Expand Up @@ -229,7 +228,7 @@ async def _execute_call(call_idx: int) -> dict[str, Any]:
return await provider.generate(
model=model,
parts=parts,
system_instruction=None,
system_instruction=options.system_instruction,
cache_name=cache_name,
response_schema=schema,
reasoning_effort=options.reasoning_effort,
Expand All @@ -242,7 +241,7 @@ async def _execute_call(call_idx: int) -> dict[str, Any]:
lambda: provider.generate(
model=model,
parts=parts,
system_instruction=None,
system_instruction=options.system_instruction,
cache_name=cache_name,
response_schema=schema,
reasoning_effort=options.reasoning_effort,
Expand Down Expand Up @@ -298,11 +297,29 @@ async def _execute_call(call_idx: int) -> dict[str, Any]:

duration_s = time.perf_counter() - start_time

conversation_state: dict[str, Any] | None = None
if wants_conversation and responses:
prompt = prompts[0] if isinstance(prompts[0], str) else str(prompts[0])
answer = responses[0].get("text")
reply = answer if isinstance(answer, str) else ""
updated_history = [
*conversation_history,
{"role": "user", "content": prompt},
{"role": "assistant", "content": reply},
]
conversation_state = {"history": updated_history}
response_id = responses[0].get("response_id")
if isinstance(response_id, str):
conversation_state["response_id"] = response_id
elif previous_response_id is not None:
conversation_state["response_id"] = previous_response_id

return ExecutionTrace(
responses=responses,
cache_name=cache_name,
duration_s=duration_s,
usage=total_usage,
conversation_state=conversation_state,
)


Expand Down
10 changes: 10 additions & 0 deletions src/pollux/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
class Options:
"""Optional execution features for `run()` and `run_many()`."""

#: Optional system-level instruction for model behavior.
system_instruction: str | None = None
#: Pydantic ``BaseModel`` subclass or JSON Schema dict for structured output.
response_schema: ResponseSchemaInput | None = None
#: Reserved — not yet wired in v1.0.
Expand All @@ -34,6 +36,14 @@ class Options:

def __post_init__(self) -> None:
"""Validate option shapes early for clear errors."""
if self.system_instruction is not None and not isinstance(
self.system_instruction, str
):
raise ConfigurationError(
"system_instruction must be a string",
hint="Pass system_instruction='You are a concise assistant.'",
)

if self.response_schema is not None and not (
isinstance(self.response_schema, dict)
or (
Expand Down
6 changes: 5 additions & 1 deletion src/pollux/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ def build_plan(request: Request) -> Plan:
if use_cache:
from pollux.cache import compute_cache_key

cache_key = compute_cache_key(config.model, sources)
cache_key = compute_cache_key(
config.model,
sources,
system_instruction=request.options.system_instruction,
)

return Plan(
request=request,
Expand Down
Loading