oi-chat 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. oi_chat-0.1.0/.env.example +12 -0
  2. oi_chat-0.1.0/.github/workflows/ci.yml +31 -0
  3. oi_chat-0.1.0/.github/workflows/release.yml +35 -0
  4. oi_chat-0.1.0/.gitignore +171 -0
  5. oi_chat-0.1.0/.pre-commit-config.yaml +15 -0
  6. oi_chat-0.1.0/.python-version +1 -0
  7. oi_chat-0.1.0/AGENTS.md +210 -0
  8. oi_chat-0.1.0/CHANGELOG.md +30 -0
  9. oi_chat-0.1.0/CLAUDE.md +1 -0
  10. oi_chat-0.1.0/LICENSE +21 -0
  11. oi_chat-0.1.0/PKG-INFO +170 -0
  12. oi_chat-0.1.0/README.md +134 -0
  13. oi_chat-0.1.0/demo.gif +0 -0
  14. oi_chat-0.1.0/demo.tape +39 -0
  15. oi_chat-0.1.0/pyproject.toml +62 -0
  16. oi_chat-0.1.0/src/oi/__init__.py +0 -0
  17. oi_chat-0.1.0/src/oi/app.py +514 -0
  18. oi_chat-0.1.0/src/oi/cli.py +90 -0
  19. oi_chat-0.1.0/src/oi/config/__init__.py +0 -0
  20. oi_chat-0.1.0/src/oi/config/loaders.py +231 -0
  21. oi_chat-0.1.0/src/oi/config/settings.py +61 -0
  22. oi_chat-0.1.0/src/oi/constants.py +26 -0
  23. oi_chat-0.1.0/src/oi/core/__init__.py +0 -0
  24. oi_chat-0.1.0/src/oi/core/chat_manager.py +176 -0
  25. oi_chat-0.1.0/src/oi/core/chat_repository.py +168 -0
  26. oi_chat-0.1.0/src/oi/core/client.py +249 -0
  27. oi_chat-0.1.0/src/oi/core/message_utils.py +175 -0
  28. oi_chat-0.1.0/src/oi/core/session.py +156 -0
  29. oi_chat-0.1.0/src/oi/core/smart_title.py +52 -0
  30. oi_chat-0.1.0/src/oi/exceptions.py +31 -0
  31. oi_chat-0.1.0/src/oi/llm_types.py +29 -0
  32. oi_chat-0.1.0/src/oi/local_commands.py +87 -0
  33. oi_chat-0.1.0/src/oi/main.py +6 -0
  34. oi_chat-0.1.0/src/oi/models.yaml +54 -0
  35. oi_chat-0.1.0/src/oi/models_template.yaml +91 -0
  36. oi_chat-0.1.0/src/oi/prompts/prompt_concise.txt +1 -0
  37. oi_chat-0.1.0/src/oi/prompts/prompt_empty.txt +0 -0
  38. oi_chat-0.1.0/src/oi/prompts/prompt_general.txt +1 -0
  39. oi_chat-0.1.0/src/oi/prompts.py +59 -0
  40. oi_chat-0.1.0/src/oi/registry.py +134 -0
  41. oi_chat-0.1.0/src/oi/renderers.py +166 -0
  42. oi_chat-0.1.0/src/oi/response_handler.py +149 -0
  43. oi_chat-0.1.0/src/oi/ui/__init__.py +0 -0
  44. oi_chat-0.1.0/src/oi/ui/chat_selector.py +320 -0
  45. oi_chat-0.1.0/src/oi/ui/image_paste.py +221 -0
  46. oi_chat-0.1.0/src/oi/ui/input_handler.py +138 -0
  47. oi_chat-0.1.0/src/oi/ui/labels.py +87 -0
  48. oi_chat-0.1.0/tests/__init__.py +0 -0
  49. oi_chat-0.1.0/tests/conftest.py +12 -0
  50. oi_chat-0.1.0/tests/fixtures/__init__.py +0 -0
  51. oi_chat-0.1.0/tests/fixtures/test_models.yaml +17 -0
  52. oi_chat-0.1.0/tests/integration/__init__.py +0 -0
  53. oi_chat-0.1.0/tests/test_main.py +17 -0
  54. oi_chat-0.1.0/tests/unit/__init__.py +0 -0
  55. oi_chat-0.1.0/tests/unit/config/__init__.py +0 -0
  56. oi_chat-0.1.0/tests/unit/config/test_loaders.py +262 -0
  57. oi_chat-0.1.0/tests/unit/core/__init__.py +0 -0
  58. oi_chat-0.1.0/tests/unit/core/test_chat_factory.py +25 -0
  59. oi_chat-0.1.0/tests/unit/core/test_client.py +226 -0
  60. oi_chat-0.1.0/tests/unit/core/test_message_utils.py +19 -0
  61. oi_chat-0.1.0/tests/unit/core/test_session.py +302 -0
  62. oi_chat-0.1.0/tests/unit/core/test_smart_title.py +66 -0
  63. oi_chat-0.1.0/tests/unit/providers/__init__.py +0 -0
  64. oi_chat-0.1.0/tests/unit/test_app.py +287 -0
  65. oi_chat-0.1.0/tests/unit/test_cli.py +96 -0
  66. oi_chat-0.1.0/tests/unit/test_registry.py +179 -0
  67. oi_chat-0.1.0/tests/unit/test_renderers.py +50 -0
  68. oi_chat-0.1.0/tests/unit/ui/test_chat_selector.py +61 -0
  69. oi_chat-0.1.0/tests/unit/ui/test_input_handler.py +74 -0
  70. oi_chat-0.1.0/todo.txt +24 -0
  71. oi_chat-0.1.0/uv.lock +3824 -0
@@ -0,0 +1,12 @@
1
+ # LLM CLI API Keys
2
+ # Copy this file to .env and add your actual API keys
3
+
4
+ OPENAI_API_KEY=your_openai_api_key_here
5
+ ANTHROPIC_API_KEY=your_anthropic_api_key_here
6
+ DEEPSEEK_API_KEY=your_deepseek_api_key_here
7
+ XAI_API_KEY=your_xai_api_key_here
8
+ GEMINI_API_KEY=your_gemini_api_key_here
9
+ OPENROUTER_API_KEY=your_openrouter_api_key_here
10
+
11
+ # Optional: Custom chat storage directory
12
+ # LLM_CLI_CHAT_DIR=/path/to/custom/chat/directory
@@ -0,0 +1,31 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ with:
19
+ enable-cache: true
20
+ - run: uv run --python ${{ matrix.python-version }} --group dev pytest -q
21
+
22
+ lint:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: astral-sh/setup-uv@v5
27
+ with:
28
+ python-version: "3.13"
29
+ enable-cache: true
30
+ - run: uvx pre-commit run --all-files
31
+ - run: uv run --group dev ty check
@@ -0,0 +1,35 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI on a version tag (e.g. `git tag v0.1.0 && git push --tags`).
4
+ # Uses PyPI Trusted Publishing (OIDC) — no API tokens stored in the repo.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v5
15
+ - run: uv build
16
+ - uses: actions/upload-artifact@v4
17
+ with:
18
+ name: dist
19
+ path: dist/
20
+
21
+ publish:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ environment:
25
+ name: pypi
26
+ url: https://pypi.org/p/oi-chat
27
+ permissions:
28
+ id-token: write # required for Trusted Publishing
29
+ steps:
30
+ - uses: actions/download-artifact@v4
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+ - uses: astral-sh/setup-uv@v5
35
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,171 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110
+ .pdm.toml
111
+ .pdm-python
112
+ .pdm-build/
113
+
114
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115
+ __pypackages__/
116
+
117
+ # Celery stuff
118
+ celerybeat-schedule
119
+ celerybeat.pid
120
+
121
+ # SageMath parsed files
122
+ *.sage.py
123
+
124
+ # Environments
125
+ .env
126
+ .venv
127
+ env/
128
+ venv/
129
+ ENV/
130
+ env.bak/
131
+ venv.bak/
132
+
133
+ # Spyder project settings
134
+ .spyderproject
135
+ .spyproject
136
+
137
+ # Rope project settings
138
+ .ropeproject
139
+
140
+ # mkdocs documentation
141
+ /site
142
+
143
+ # mypy
144
+ .mypy_cache/
145
+ .dmypy.json
146
+ dmypy.json
147
+
148
+ # Pyre type checker
149
+ .pyre/
150
+
151
+ # pytype static type analyzer
152
+ .pytype/
153
+
154
+ # Cython debug symbols
155
+ cython_debug/
156
+
157
+ # PyCharm
158
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
161
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162
+ #.idea/
163
+ oi/prompts/*
164
+ !oi/prompts/prompt_general.txt
165
+ !oi/prompts/prompt_concise.txt
166
+
167
+ # Temporary/scratch files
168
+ tmp.*
169
+
170
+ # Pre-commit cache
171
+ .pre-commit-cache/
@@ -0,0 +1,15 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: check-yaml
6
+ - id: check-added-large-files
7
+ - id: check-merge-conflict
8
+ - id: check-case-conflict
9
+
10
+ - repo: https://github.com/astral-sh/ruff-pre-commit
11
+ rev: v0.13.2
12
+ hooks:
13
+ - id: ruff
14
+ args: [--fix]
15
+ - id: ruff-format
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,210 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commit Messages
6
+
7
+ Use [Conventional Commits](https://www.conventionalcommits.org/) style:
8
+
9
+ ```
10
+ <type>(<optional scope>): <description>
11
+ ```
12
+
13
+ Common types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`
14
+
15
+ Examples:
16
+ - `feat(models): add support for Gemini 2.0`
17
+ - `fix(client): handle retry on rate limit errors`
18
+ - `refactor: extract ChatSelector into ui module`
19
+
20
+ ## Development Commands
21
+
22
+ **Testing:**
23
+ ```bash
24
+ uv run pytest # Run all tests
25
+ uv run pytest tests/test_main.py # Run specific test file
26
+ ```
27
+
28
+ **Code Quality:**
29
+ ```bash
30
+ uv run ty check # Type checking (ruff formatting/linting handled by pre-commit)
31
+ ```
32
+
33
+ **Installation & Setup:**
34
+ ```bash
35
+ # Local development with uv
36
+ uv install
37
+
38
+ # Install dev dependencies
39
+ uv install --group dev
40
+
41
+ # Set up pre-commit hooks
42
+ uv run pre-commit install
43
+
44
+ # Add new dependencies
45
+ uv add <package-name> # Add a new dependency
46
+
47
+ # Global installation
48
+ pipx install -e . # Install from local copy
49
+ pipx install --force -e . # Reinstall after changes
50
+ ```
51
+
52
+ **Running the application:**
53
+ ```bash
54
+ uv run oi # CLI interface with default settings
55
+ uv run oi -P concise -m sonnet # Use specific prompt and model
56
+ ```
57
+
58
+ ## Architecture Overview (Post-Refactoring)
59
+
60
+ **Directory Structure:**
61
+ ```
62
+ src/oi/
63
+ ├── core/ # Core business logic
64
+ │ ├── client.py # LLMClient - API calls & retry logic
65
+ │ ├── session.py # Chat & ChatMetadata - data models + Chat.create_new()
66
+ │ ├── chat_manager.py # ChatManager - CRUD operations
67
+ │ ├── chat_repository.py # ChatRepository - filesystem persistence
68
+ │ ├── message_utils.py # Message serialization & history helpers
69
+ │ └── smart_title.py # Smart title generation
70
+ ├── config/ # Configuration management
71
+ │ ├── settings.py # Config class + user config (JSON) management
72
+ │ └── loaders.py # YAML model configuration loading & merging
73
+ ├── ui/ # User interface components
74
+ │ ├── input_handler.py # InputHandler - prompt_toolkit integration
75
+ │ ├── chat_selector.py # ChatSelector - interactive chat picker
76
+ │ ├── image_paste.py # PasteStore (images + long text) + PillProcessor + clipboard image reader
77
+ │ └── labels.py # Shared ANSI/Rich/prompt-toolkit label styling
78
+ ├── llm_types.py # Shared chat/model capability dataclasses
79
+ ├── app.py # Main application orchestration + ChatLoopContext
80
+ ├── cli.py # Command-line argument parsing
81
+ ├── main.py # Entry point (delegates to app.py)
82
+ ├── constants.py # All constants & UI config
83
+ ├── exceptions.py # Custom exception classes
84
+ ├── local_commands.py # Local in-chat slash command registry + completion
85
+ ├── prompts.py # Prompt file loading
86
+ ├── registry.py # ModelRegistry - alias + capability management (single config load)
87
+ └── renderers.py # Response rendering (StyledRenderer)
88
+ ```
89
+
90
+ **Multi-provider LLM Client:**
91
+ Supports OpenAI, Anthropic, DeepSeek, Google Gemini, xAI, and OpenRouter through Pydantic AI's `direct` APIs with a unified interface.
92
+
93
+ **Centralized Model Registry:**
94
+ - `ModelRegistry` loads merged config once via `load_merged_model_config()`, then derives both the model map and capabilities from it
95
+ - Providers are "dumb" API clients - no hardcoded model definitions
96
+ - Default model configurable via `aliases.default` in YAML
97
+ - Cross-provider aliases supported
98
+
99
+ **Model Configuration:**
100
+ - **Minimal default config**: `src/oi/models.yaml` contains only latest SOTA models with date-free aliases
101
+ - **Auto-generated user config**: `~/.config/oi/models.yaml` created on first run from `models_template.yaml`
102
+ - **Deep merge**: User config merges with defaults at model property level (can add just `extra_params` without repeating all capabilities)
103
+ - **YAML anchors**: Top-level keys starting with `_` are ignored (prevents anchors from being treated as providers)
104
+ - **extra_params support**: Model-specific settings (OpenRouter quantization, OpenAI `openai_reasoning_effort`, etc.) merged into `model_settings` before API calls
105
+ - Per-model settings: `max_tokens`, `supports_search`, `supports_thinking`, `supports_vision`, `extra_params`
106
+
107
+ **Configuration & Prompts:**
108
+ Dual-location system:
109
+ 1. User config directory (`~/.config/oi/prompts/`) - takes precedence
110
+ 2. Package built-in prompts (`src/oi/prompts/`)
111
+
112
+ Format: `prompt_[name].txt`, loaded via `prompts.py:read_system_message_from_file()`
113
+
114
+ **Chat Management:**
115
+ - Rich-based interactive chat selection via `ui/chat_selector.py`
116
+ - Automatic session persistence with metadata in `core/session.py`
117
+ - Smart title generation (triggers after 8+ messages)
118
+ - Auto-save functionality
119
+
120
+ **Headless Mode:**
121
+ - `-p MESSAGE` sends one turn and exits; composes with `-c` / `-r ID` to follow up against existing chats (appends in-place, same chat ID)
122
+ - `--ephemeral` skips all persistence. Combined with `-c` / `-r` it runs a scratch turn against the existing chat's context without modifying it. Works in interactive mode too — the save gate is in `run_chat_loop` via `ctx.ephemeral`
123
+ - `run_headless_turn()` in `app.py` is the headless entry point; `main()` branches to it when `args.prompt is not None`
124
+ - Output cleanups for pipe-friendliness: `AI:` label hidden via `ChatOptions.show_assistant_label=False`, `Loaded chat:` / "No previous chats" chatter suppressed via `handle_chat_selection(quiet=True)`. Thinking traces still render unless `--hide-thinking` is passed (compose them for clean stdout)
125
+ - `-r` without an ID errors in headless — interactive selector is unavailable
126
+
127
+ **Streaming & Output:**
128
+ - `StyledRenderer` is the only renderer — provides styled thinking traces (NOT markdown rendering!)
129
+ - Shared label/color definitions live in `ui/labels.py` and are reused by plain prints, Rich output, and the prompt label
130
+ - Rich console with `highlight=False` to prevent number styling in LLM output
131
+ - Real-time streaming with interrupt handling
132
+
133
+ **Paste Pills (images + long text):**
134
+ - One unified `PasteStore` in `ui/image_paste.py` allocates Unicode PUA sentinel chars for both kinds of pastes; one sentinel per entry so backspace/vim `x`/word motions treat the pill atomically
135
+ - `PillProcessor` expands each sentinel at display time: image sentinels render as `[Image #N] `, text-paste sentinels as `[Paste #N (L lines)] `. Images and pastes are numbered independently, by first occurrence order in the buffer, so deleting one renumbers the rest
136
+ - Image path: `Alt+V` reads the clipboard (via `wl-paste` / `xclip`) and inserts an image sentinel. Binding is only registered when the active model has `supports_vision: true`. `Ctrl+V` is unusable because most terminals (Ghostty, Konsole, iTerm2, …) hijack it for `paste_from_clipboard`
137
+ - Long-text path: `Keys.BracketedPaste` is intercepted in `InputHandler`; pastes that hit `PASTE_LINE_THRESHOLD` (6 lines) **or** `PASTE_CHAR_THRESHOLD` (400 chars) become a single text-paste sentinel, shorter pastes are inserted verbatim. The char limit catches long single-line paragraphs that wrap across many rendered rows. This works around a prompt_toolkit limitation — its diff-based renderer can't progressively commit rows to the scrollback buffer, so any content that scrolls past terminal height gets permanently clobbered. Pills keep the buffer visually short. The pill label still shows lines (source lines match the user's mental model of what they copied, even though chars drive the trigger)
138
+ - On submit, `PasteStore.split()` walks the buffer: text-paste sentinels expand inline into the surrounding text, image sentinels become `BinaryContent` parts. Returns `str` (text-only) or `list[str | BinaryContent]` (mixed). `UserPromptPart` takes either; pydantic-ai passes through to providers
139
+ - `flatten_history` only needs to handle images (text pastes are just text by submit time) — renders `[Image #N]` placeholders for replay of mixed-content messages
140
+
141
+ **Local Slash Commands:**
142
+ - Local in-chat commands are defined in `local_commands.py`, not inline in `InputHandler`
143
+ - `InputHandler` wires slash command completion through prompt-toolkit
144
+ - Slash command completion uses `CompleteStyle.READLINE_LIKE`, so completion is `Tab`-triggered and rendered in a readline-like way instead of a dropdown menu
145
+ - Unknown slash commands are still rejected in `app.py` after submit so they never get sent to the model
146
+
147
+ **Key Components:**
148
+ - `LLMClient` (core/client.py) - High-level API client with retry logic
149
+ - `ChatManager` (core/chat_manager.py) - Session persistence & management
150
+ - `Chat`/`ChatMetadata` (core/session.py) - Data models
151
+ - `ChatSelector` (ui/chat_selector.py) - Interactive chat selection
152
+ - `InputHandler` (ui/input_handler.py) - User input handling
153
+ - `local_commands.py` - Slash command definitions + completion helpers
154
+ - `ui/labels.py` - Shared label text and styling helpers
155
+ - `ModelRegistry` (registry.py) - Central model/provider management
156
+ - `ResponseHandler` (response_handler.py) - Streaming coordination
157
+
158
+ **Main Function Structure:**
159
+ Located in `app.py`, broken into logical functions:
160
+ - `parse_arguments()` - CLI parsing (from cli.py)
161
+ - `setup_configuration()` - Returns a `ChatLoopContext` bundling all components
162
+ - `handle_chat_selection()` - Chat loading
163
+ - `Chat.create_new()` - New session creation (classmethod on `Chat`)
164
+ - `run_chat_loop(chat, ctx)` - Main interaction (takes `ChatLoopContext`)
165
+ - `run_headless_turn(args, ctx, registry)` - Single-turn headless path (used when `-p` is set)
166
+ - `main()` - High-level orchestration
167
+
168
+ **Key Constants:**
169
+ Mostly centralized in `constants.py`:
170
+ - `MIN_MESSAGES_FOR_SMART_TITLE = 8`
171
+ - `DEFAULT_PAGE_SIZE = 10`
172
+ - UI navigation keys
173
+
174
+ Conversation and status labels are centralized in `ui/labels.py`:
175
+ - `USER_LABEL`, `AI_LABEL`, `SYSTEM_LABEL`
176
+ - `INFO_LABEL`, `WARNING_LABEL`, `ERROR_LABEL`
177
+
178
+ **Common Gotchas:**
179
+ 1. Add models to `models.yaml`, not provider classes
180
+ 2. `StyledRenderer` is for styled thinking traces, NOT markdown rendering
181
+ 3. Default model from YAML `aliases.default`, not hardcoded
182
+ 4. No bespoke provider classes—add/update models via YAML aliases instead
183
+ 5. Thinking traces:
184
+ - OpenAI reasoning models automatically receive `openai_reasoning_summary="detailed"` when thinking is enabled so we can render their reasoning summaries.
185
+ - OpenAI reasoning models also set `openai_reasoning_effort="medium"` by default to satisfy the API requirement.
186
+ - Anthropic models default to `anthropic_thinking={"type": "adaptive"}` when thinking is enabled (via `setdefault` in client.py). Adaptive thinking means the model decides how much to think.
187
+ - **Claude Haiku 4.5** overrides this via `extra_params: {anthropic_thinking: {type: enabled, budget_tokens: 2048}}` in `models.yaml` because it still requires the explicit budget.
188
+ - Google Gemini models default to `google_thinking_config={"include_thoughts": True}` when thinking is enabled so their thoughts stream into the UI.
189
+ 6. Reasoning-focused OpenAI models (gpt-5, o-series) should be defined under the `openai-responses` provider section so the Responses API (with thinking traces) is used.
190
+ 7. `--search` wires up Pydantic AI's `WebSearchTool` only for providers that support it (OpenAI Responses, Anthropic, Gemini, xAI). OpenRouter models automatically switch to their `:online` variant and add the `web` plugin so search works there too; other providers simply ignore the flag.
191
+ 8. Rich console has `highlight=False` to prevent auto-styling numbers
192
+ 9. User config functions (`load_user_config`, `update_user_config`) live in `config/settings.py`, not a separate file
193
+ 10. Prompts loaded from `src/oi/prompts/` directory, not a Python package
194
+ 10. Custom exceptions in `exceptions.py` for proper error handling
195
+ 11. Conversation/status label text and colors live in `ui/labels.py`, not `constants.py`
196
+ 12. Local slash commands are completed from `local_commands.py`; if you add one, update the command registry there
197
+ 13. Slash command completion is readline-like `Tab` completion, not a dropdown selector UI
198
+ 14. Paste pills (both images and long text) use Unicode PUA sentinel chars in the input buffer; display-only pill expansion via a prompt_toolkit `Processor`. Text pastes expand inline on submit, images become `BinaryContent` parts. Long-text threshold is a fixed `PASTE_LINE_THRESHOLD` in `input_handler.py` (not a function of terminal size — the true failure mode is rendered-rows vs scrollback, which a source-line threshold can only approximate, so the constant is the honest choice). Don't bind `Ctrl+V` — terminals hijack it for paste
199
+
200
+ **Quick Tests:**
201
+ ```bash
202
+ uv run oi --help # Smoke test
203
+ uv run python -c "from oi.registry import ModelRegistry; print(list(ModelRegistry().get_available_models().keys()))" # Test model loading
204
+
205
+ # Headless e2e smoke — `--ephemeral` guarantees no chat dir is written or modified.
206
+ # Use a cheap/fast model and `--no-thinking` for deterministic, grep-friendly output.
207
+ uv run oi -p "say only the word PONG" --ephemeral -m haiku --no-thinking
208
+ ```
209
+
210
+ Do NOT reach for `-c --ephemeral -p "..."` as a casual smoke test — `-c` loads the user's actual latest chat, so even with `--ephemeral` (no save) you still send their full real conversation to the API and then prompt the model with something unrelated. Waste of tokens, confusing for the model. If you really need to exercise the multi-turn path, create an explicit fixture chat first.
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
7
+ while pre-1.0, minor version bumps may include breaking changes (CLI flags,
8
+ config/`models.yaml` format, alias names).
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.1.0] - 2026-05-24
13
+
14
+ Initial public release.
15
+
16
+ ### Changed
17
+
18
+ - Rebranded from `llm-cli` to `oi`: command, Python package, config/data
19
+ directories, and PyPI distribution name (`oi-chat`).
20
+ - Upgraded `pydantic-ai` to 1.100 and migrated from `builtin_tools` to the
21
+ `native_tools` API.
22
+ - Use the non-deprecated `google` provider prefix (was `google-gla`).
23
+
24
+ ### Added
25
+
26
+ - PyPI packaging metadata (license, classifiers, project URLs).
27
+ - Support for Python 3.10–3.13.
28
+
29
+ [Unreleased]: https://github.com/dansclearov/oi/compare/v0.1.0...HEAD
30
+ [0.1.0]: https://github.com/dansclearov/oi/releases/tag/v0.1.0
@@ -0,0 +1 @@
1
+ AGENTS.md
oi_chat-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Dan Sclearov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.