ruby_coded 0.2.2 → 0.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +84 -3
- data/lib/ruby_coded/auth/jwt_decoder.rb +14 -0
- data/lib/ruby_coded/chat/app.rb +16 -4
- data/lib/ruby_coded/chat/codex_bridge/error_handling.rb +68 -10
- data/lib/ruby_coded/chat/codex_bridge/sse_parser.rb +20 -0
- data/lib/ruby_coded/chat/codex_models.rb +36 -7
- data/lib/ruby_coded/chat/command_handler/custom_commands.rb +91 -0
- data/lib/ruby_coded/chat/command_handler/model_commands.rb +8 -1
- data/lib/ruby_coded/chat/command_handler.rb +64 -36
- data/lib/ruby_coded/chat/help.txt +0 -20
- data/lib/ruby_coded/chat/renderer/model_selector.rb +4 -1
- data/lib/ruby_coded/chat/renderer/status_bar.rb +7 -0
- data/lib/ruby_coded/chat/state/context_window.rb +59 -0
- data/lib/ruby_coded/chat/state/message_token_tracking.rb +16 -0
- data/lib/ruby_coded/chat/state.rb +19 -3
- data/lib/ruby_coded/commands/catalog.rb +170 -0
- data/lib/ruby_coded/commands/command_definition.rb +30 -0
- data/lib/ruby_coded/commands/core_provider.rb +94 -0
- data/lib/ruby_coded/commands/markdown_loader.rb +101 -0
- data/lib/ruby_coded/commands/markdown_provider.rb +45 -0
- data/lib/ruby_coded/commands/plugin_provider.rb +38 -0
- data/lib/ruby_coded/commands.rb +8 -0
- data/lib/ruby_coded/plugins/command_completion/state_extension.rb +5 -18
- data/lib/ruby_coded/version.rb +1 -1
- data/lib/ruby_coded.rb +1 -0
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 458d56c13426096bb1d553aad86f7d2502a94022cc53de44ca7c74ab48cf2b5a
|
|
4
|
+
data.tar.gz: 128c5c97ccf42e93a7671ad5641d85c3f0c80f425c74506f0f22f7a4d4e95652
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a244f39012eb45e2396f61a94fb2b14f23ffd1269c4b235a1ea7ce456763cd674f13e51c4cc3584f95ab4d15517ce63aaff1c113848b2ff467aae70606770726
|
|
7
|
+
data.tar.gz: b1ba816f5deb3e7051764602bb2bb3afcac031af219f6523eb1d54f021d77fc97844068952c914a2d7f1bd441f62968141678ec09b169bf6d855cbed3f1c0848
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-04-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Pro-only Codex model handling**: `gpt-5.3-codex-spark` and `gpt-5.2-codex` are tagged as `pro_only` in the Codex model catalog and render with a `(Pro only)` marker in the `/model` selector. Plus/free JWT plan claims now hide Pro-only models from the selector, while unknown plans keep the previous behavior to avoid breaking Pro users.
|
|
8
|
+
- **Automatic Codex model fallback**: When the Codex backend rejects a model with `not supported when using Codex with a ChatGPT account`, the request is transparently retried against `gpt-5.4` and the user is informed via a system message.
|
|
9
|
+
- **Provider metadata in `/model`**: `CodexModel` now exposes provider metadata so the selector shows `(openai)` instead of `(unknown)`.
|
|
10
|
+
- **Unified command catalog**: New `RubyCoded::Commands` module with a central `Catalog`, `CommandDefinition`, and provider abstraction (`CoreProvider`, `PluginProvider`, `MarkdownProvider`) consolidating all slash commands in a single source of truth, replacing the hand-rolled `help.txt` and ad-hoc registrations.
|
|
11
|
+
- **Markdown custom commands**: Users can now drop `*.md` files under `~/.ruby_coded/commands/` (or the project-local `.ruby_coded/commands/`) to define reusable slash commands with front-matter metadata, loaded via the new `MarkdownLoader` / `MarkdownProvider`.
|
|
12
|
+
- **Context window tracking**: New `State::ContextWindow` module plus status bar integration that shows live context usage per model, driven by per-turn token tracking.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **Status bar polish**: The status bar now renders context window usage alongside the model/auth indicators and hides noisy fields when data is unavailable.
|
|
17
|
+
- **Command handler refactor**: `CommandHandler` is now powered by the unified catalog (core + plugin + markdown providers) and delegates custom command execution to the new `CustomCommands` mixin.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Context window indicator for ChatGPT Plus/Pro users**: `CodexBridge` now parses the `response.completed` SSE event and feeds `input`/`output`/`reasoning`/`cached` token usage into `State`, mirroring the API path. `session_context_tokens_used` reflects the last turn's live prompt size via the new `State#last_turn_context_tokens` helper, eliminating the double-counting that happened because both bridges re-send the full history on each request.
|
|
22
|
+
|
|
3
23
|
## [0.2.2] - 2026-04-17
|
|
4
24
|
|
|
5
25
|
### Fixed
|
data/README.md
CHANGED
|
@@ -28,7 +28,7 @@ An AI-powered terminal coding assistant built in Ruby. Chat with LLMs, let an ag
|
|
|
28
28
|
- **Tool confirmation** — Write and dangerous operations require explicit approval; safe operations (read, list) run automatically
|
|
29
29
|
- **Token & cost tracking** — Live status bar showing token usage, estimated session cost, and auth mode indicator
|
|
30
30
|
- **Plugin system** — Extend the chat with custom state, input handlers, renderer overlays, and commands
|
|
31
|
-
- **Slash commands** — `/agent`, `/plan`, `/model`, `/login`, `/history`, `/tokens`, `/help`, and more
|
|
31
|
+
- **Slash commands** — `/agent`, `/plan`, `/model`, `/login`, `/history`, `/tokens`, `/commands reload`, `/commands list`, `/help`, and more
|
|
32
32
|
|
|
33
33
|
## Requirements
|
|
34
34
|
|
|
@@ -62,6 +62,8 @@ On first launch you'll be asked to authenticate with a provider. After that, you
|
|
|
62
62
|
| `/plan save` | Save the current plan to a file |
|
|
63
63
|
| `/model` | Switch to a different model |
|
|
64
64
|
| `/login` | Authenticate with a provider (OpenAI, Anthropic) |
|
|
65
|
+
| `/commands reload` | Reload custom markdown commands from the current project |
|
|
66
|
+
| `/commands list` | List currently loaded custom markdown commands |
|
|
65
67
|
| `/tokens` | Show detailed token usage breakdown |
|
|
66
68
|
| `/history` | Show conversation history |
|
|
67
69
|
| `/clear` | Clear the conversation |
|
|
@@ -87,6 +89,86 @@ Safe tools run without asking. Confirm and dangerous tools show the operation de
|
|
|
87
89
|
|
|
88
90
|
Plan mode restricts the model to read-only tools and a planning-oriented system prompt. The model will analyze your project and propose a structured plan. If the model needs clarification, it presents interactive options you can select or answer with custom text.
|
|
89
91
|
|
|
92
|
+
### Custom commands
|
|
93
|
+
|
|
94
|
+
You can define your own project-specific slash commands using Markdown files inside:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
.ruby_coded/commands/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Each `.md` file represents one custom command. RubyCoded loads these commands into:
|
|
101
|
+
|
|
102
|
+
- the slash-command autocomplete list
|
|
103
|
+
- `/help`
|
|
104
|
+
- command execution
|
|
105
|
+
|
|
106
|
+
#### File format
|
|
107
|
+
|
|
108
|
+
Use YAML frontmatter followed by the command body:
|
|
109
|
+
|
|
110
|
+
```md
|
|
111
|
+
---
|
|
112
|
+
command: /review-auth
|
|
113
|
+
description: Review auth implementation
|
|
114
|
+
usage: /review-auth [file]
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
Review the authentication implementation and suggest improvements.
|
|
118
|
+
Focus on risks, bugs, and refactoring opportunities.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Required fields
|
|
122
|
+
|
|
123
|
+
- `command` — command name, must start with `/`
|
|
124
|
+
- `description` — short description shown in autocomplete and help
|
|
125
|
+
|
|
126
|
+
#### Optional fields
|
|
127
|
+
|
|
128
|
+
- `usage` — custom usage text shown in `/help`
|
|
129
|
+
|
|
130
|
+
#### How it works
|
|
131
|
+
|
|
132
|
+
The Markdown body is used as the prompt template for the command. For example:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
/review-auth lib/ruby_coded/chat/command_handler.rb
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
RubyCoded will send the command body to the model and append the extra user input as additional context.
|
|
139
|
+
|
|
140
|
+
#### Managing commands
|
|
141
|
+
|
|
142
|
+
Custom commands are loaded from the current project. After adding, editing, or deleting command files, you can manage them without restarting the app.
|
|
143
|
+
|
|
144
|
+
Reload command definitions:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
/commands reload
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The reload command reports:
|
|
151
|
+
|
|
152
|
+
- how many custom commands were added
|
|
153
|
+
- how many were removed
|
|
154
|
+
- how many are currently available
|
|
155
|
+
- how many invalid files were ignored
|
|
156
|
+
- how many conflicting commands were ignored
|
|
157
|
+
- invalid file names
|
|
158
|
+
- conflicting command names
|
|
159
|
+
|
|
160
|
+
List currently loaded custom commands:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
/commands list
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### Notes
|
|
167
|
+
|
|
168
|
+
- Core commands take precedence over custom commands.
|
|
169
|
+
- Plugin commands also take precedence over custom Markdown commands.
|
|
170
|
+
- Invalid Markdown command files are ignored during reload.
|
|
171
|
+
|
|
90
172
|
## Keyboard shortcuts
|
|
91
173
|
|
|
92
174
|
| Key | Action |
|
|
@@ -116,10 +198,9 @@ bundle exec exe/ruby_coded
|
|
|
116
198
|
|
|
117
199
|
## What's next
|
|
118
200
|
|
|
119
|
-
- Find a way to update the autocomplete plugin when a new command is added
|
|
120
201
|
- Display context window size (depending on the model)
|
|
121
202
|
- UI element to indicate the AI is performing a task
|
|
122
|
-
-
|
|
203
|
+
- Improve custom commands further (validation, conflict reporting, management UX)
|
|
123
204
|
- Skills implementation
|
|
124
205
|
- Implement Google Auth for Gemini
|
|
125
206
|
- Local LLM support
|
|
@@ -24,6 +24,20 @@ module RubyCoded
|
|
|
24
24
|
payload = decode(token)
|
|
25
25
|
payload&.dig(JWT_CLAIM_PATH, "chatgpt_account_id")
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
PLAN_CLAIM_KEYS = %w[chatgpt_plan_type planType plan_type].freeze
|
|
29
|
+
|
|
30
|
+
def self.extract_plan_type(token)
|
|
31
|
+
payload = decode(token)
|
|
32
|
+
return nil unless payload
|
|
33
|
+
|
|
34
|
+
auth_claims = payload[JWT_CLAIM_PATH].is_a?(Hash) ? payload[JWT_CLAIM_PATH] : {}
|
|
35
|
+
PLAN_CLAIM_KEYS.each do |key|
|
|
36
|
+
value = auth_claims[key] || payload[key]
|
|
37
|
+
return value.to_s.downcase if value
|
|
38
|
+
end
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
27
41
|
end
|
|
28
42
|
end
|
|
29
43
|
end
|
data/lib/ruby_coded/chat/app.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "command_handler"
|
|
|
13
13
|
require_relative "llm_bridge"
|
|
14
14
|
require_relative "codex_bridge"
|
|
15
15
|
require_relative "codex_models"
|
|
16
|
+
require_relative "../commands/catalog"
|
|
16
17
|
require_relative "../auth/credentials_store"
|
|
17
18
|
require_relative "../auth/jwt_decoder"
|
|
18
19
|
require_relative "../auth/pkce"
|
|
@@ -40,7 +41,12 @@ module RubyCoded
|
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def build_components!
|
|
43
|
-
@
|
|
44
|
+
@command_catalog = RubyCoded::Commands::Catalog.new(
|
|
45
|
+
project_root: Dir.pwd,
|
|
46
|
+
plugin_registry: RubyCoded.plugin_registry
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@state = State.new(model: @model, command_catalog: @command_catalog)
|
|
44
50
|
@credentials_store = Auth::CredentialsStore.new(user_config: @user_config)
|
|
45
51
|
@llm_bridge = create_bridge
|
|
46
52
|
@input_handler = InputHandler.new(@state)
|
|
@@ -96,8 +102,14 @@ module RubyCoded
|
|
|
96
102
|
end
|
|
97
103
|
|
|
98
104
|
def build_command_handler
|
|
99
|
-
CommandHandler.new(
|
|
100
|
-
|
|
105
|
+
CommandHandler.new(
|
|
106
|
+
@state,
|
|
107
|
+
llm_bridge: @llm_bridge,
|
|
108
|
+
user_config: @user_config,
|
|
109
|
+
credentials_store: @credentials_store,
|
|
110
|
+
auth_manager: @auth_manager,
|
|
111
|
+
command_catalog: @command_catalog
|
|
112
|
+
)
|
|
101
113
|
end
|
|
102
114
|
|
|
103
115
|
def announce_model_fallback
|
|
@@ -150,6 +162,7 @@ module RubyCoded
|
|
|
150
162
|
end
|
|
151
163
|
|
|
152
164
|
def recreate_bridge!
|
|
165
|
+
@command_catalog.reload!
|
|
153
166
|
agentic = @llm_bridge.agentic_mode
|
|
154
167
|
plan = @llm_bridge.plan_mode
|
|
155
168
|
@llm_bridge = create_bridge
|
|
@@ -157,7 +170,6 @@ module RubyCoded
|
|
|
157
170
|
@llm_bridge.toggle_plan_mode!(plan) if plan
|
|
158
171
|
@command_handler = build_command_handler
|
|
159
172
|
end
|
|
160
|
-
|
|
161
173
|
end
|
|
162
174
|
end
|
|
163
175
|
end
|
|
@@ -4,12 +4,15 @@ module RubyCoded
|
|
|
4
4
|
module Chat
|
|
5
5
|
class CodexBridge
|
|
6
6
|
# Retry logic and error message formatting for the Codex API client.
|
|
7
|
+
# rubocop:disable Metrics/ModuleLength
|
|
7
8
|
module ErrorHandling
|
|
8
9
|
AGENT_SWITCH_PATTERN = /
|
|
9
10
|
\b(implement|go[ ]ahead|proceed|execut|ejecutar?|comenz|
|
|
10
11
|
comienz|hazlo|constru[iy]|adelante|dale|do[ ]it|build[ ]it)\b
|
|
11
12
|
/ix
|
|
12
13
|
|
|
14
|
+
UNSUPPORTED_MODEL_PATTERN = /not supported when using Codex with a ChatGPT account/i
|
|
15
|
+
|
|
13
16
|
private
|
|
14
17
|
|
|
15
18
|
def build_connection
|
|
@@ -43,19 +46,56 @@ module RubyCoded
|
|
|
43
46
|
"Plan mode disabled — switching to agent mode to implement the plan.")
|
|
44
47
|
end
|
|
45
48
|
|
|
46
|
-
def attempt_with_retries(input, retries = 0)
|
|
49
|
+
def attempt_with_retries(input, retries = 0, fallback_attempted: false)
|
|
47
50
|
perform_codex_request(input)
|
|
48
51
|
rescue Tools::AgentCancelledError, Tools::AgentIterationLimitError, Tools::ToolRejectedError => e
|
|
49
52
|
@state.add_message(:system, e.message)
|
|
50
53
|
rescue CodexAPIError => e
|
|
51
|
-
|
|
54
|
+
handle_codex_api_error(e, input, retries, fallback_attempted)
|
|
52
55
|
rescue Faraday::TooManyRequestsError => e
|
|
53
|
-
|
|
54
|
-
@state.fail_last_assistant(e, friendly_message: rate_limit_message(e))
|
|
56
|
+
handle_rate_limit_error(e, retries, input, fallback_attempted)
|
|
55
57
|
rescue StandardError => e
|
|
56
58
|
@state.fail_last_assistant(e, friendly_message: "Codex API error: #{e.message}")
|
|
57
59
|
end
|
|
58
60
|
|
|
61
|
+
def handle_codex_api_error(error, input, retries, fallback_attempted)
|
|
62
|
+
return fail_codex_request(error) unless should_fallback_to_default_model?(error, fallback_attempted)
|
|
63
|
+
|
|
64
|
+
switch_to_default_model!(error)
|
|
65
|
+
attempt_with_retries(input, retries, fallback_attempted: true)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fail_codex_request(error)
|
|
69
|
+
@state.fail_last_assistant(error, friendly_message: codex_error_message(error))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def handle_rate_limit_error(error, retries, input, fallback_attempted)
|
|
73
|
+
next_retries = handle_rate_limit_retry(error, retries)
|
|
74
|
+
return attempt_with_retries(input, next_retries, fallback_attempted: fallback_attempted) if next_retries
|
|
75
|
+
|
|
76
|
+
@state.fail_last_assistant(error, friendly_message: rate_limit_message(error))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def should_fallback_to_default_model?(error, already_attempted)
|
|
80
|
+
return false if already_attempted
|
|
81
|
+
return false unless error.status == 400
|
|
82
|
+
return false unless error.message.match?(UNSUPPORTED_MODEL_PATTERN)
|
|
83
|
+
|
|
84
|
+
@model != DEFAULT_MODEL
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def switch_to_default_model!(error)
|
|
88
|
+
previous_model = @model
|
|
89
|
+
@model = DEFAULT_MODEL
|
|
90
|
+
@state.model = DEFAULT_MODEL
|
|
91
|
+
@state.reset_last_assistant_content
|
|
92
|
+
@state.add_message(
|
|
93
|
+
:system,
|
|
94
|
+
"Model '#{previous_model}' requires ChatGPT Pro. " \
|
|
95
|
+
"Falling back to #{DEFAULT_MODEL} and retrying. Detail: #{error.message}"
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
59
99
|
def handle_rate_limit_retry(error, retries)
|
|
60
100
|
return unless retries < MAX_RATE_LIMIT_RETRIES && !@cancel_requested
|
|
61
101
|
|
|
@@ -69,18 +109,35 @@ module RubyCoded
|
|
|
69
109
|
end
|
|
70
110
|
|
|
71
111
|
def codex_error_message(error)
|
|
112
|
+
return unsupported_model_message(error) if unsupported_model_error?(error)
|
|
113
|
+
|
|
114
|
+
status_message(error) || "Codex API error: #{error.message}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def unsupported_model_error?(error)
|
|
118
|
+
error.status == 400 && error.message.match?(UNSUPPORTED_MODEL_PATTERN)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def unsupported_model_message(error)
|
|
122
|
+
"The selected model requires ChatGPT Pro. " \
|
|
123
|
+
"Use /model to pick one without the 'Pro only' tag. (#{error.message})"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def status_message(error)
|
|
72
127
|
case error.status
|
|
73
|
-
when
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
when 403
|
|
77
|
-
"Access denied. Your ChatGPT subscription may not include Codex access. (#{error.message})"
|
|
128
|
+
when 400 then "Codex API error: #{error.message}"
|
|
129
|
+
when 401 then authentication_error_message(error)
|
|
130
|
+
when 403 then "Access denied. Your ChatGPT subscription may not include Codex access. (#{error.message})"
|
|
78
131
|
when 404 then "Codex endpoint not found. The API may have changed. (#{error.message})"
|
|
79
132
|
when 429 then rate_limit_message(error)
|
|
80
|
-
else "Codex API error: #{error.message}"
|
|
81
133
|
end
|
|
82
134
|
end
|
|
83
135
|
|
|
136
|
+
def authentication_error_message(error)
|
|
137
|
+
"Authentication failed. Your OAuth session may have expired. " \
|
|
138
|
+
"Try /login to re-authenticate. (#{error.message})"
|
|
139
|
+
end
|
|
140
|
+
|
|
84
141
|
def rate_limit_message(error)
|
|
85
142
|
<<~MSG.strip
|
|
86
143
|
ChatGPT usage limit reached. This may be a 5-hour or weekly limit on your Plus/Pro subscription.
|
|
@@ -88,6 +145,7 @@ module RubyCoded
|
|
|
88
145
|
MSG
|
|
89
146
|
end
|
|
90
147
|
end
|
|
148
|
+
# rubocop:enable Metrics/ModuleLength
|
|
91
149
|
end
|
|
92
150
|
end
|
|
93
151
|
end
|
|
@@ -5,6 +5,7 @@ module RubyCoded
|
|
|
5
5
|
class CodexBridge
|
|
6
6
|
# Parses Server-Sent Events from the Codex streaming response and
|
|
7
7
|
# dispatches content deltas, tool calls, and completion signals.
|
|
8
|
+
# rubocop:disable Metrics/ModuleLength
|
|
8
9
|
module SSEParser
|
|
9
10
|
StreamContext = Struct.new(
|
|
10
11
|
:assistant_text, :pending_tool_calls, :buffer, :raw_body, :status, keyword_init: true
|
|
@@ -70,6 +71,7 @@ module RubyCoded
|
|
|
70
71
|
when "response.function_call_arguments.delta" then handle_function_args_delta(event, pending_tool_calls)
|
|
71
72
|
when "response.function_call_arguments.done" then handle_function_call_done(event, pending_tool_calls)
|
|
72
73
|
when "response.output_item.added" then handle_output_item_added(event, pending_tool_calls)
|
|
74
|
+
when "response.completed" then handle_response_completed(event)
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
|
|
@@ -107,6 +109,23 @@ module RubyCoded
|
|
|
107
109
|
tc[:arguments] = event["arguments"] || tc[:arguments] if tc
|
|
108
110
|
end
|
|
109
111
|
|
|
112
|
+
# The Responses API emits a `response.completed` event at the end of the
|
|
113
|
+
# stream carrying the `usage` block. We mirror what `LLMBridge` does and
|
|
114
|
+
# feed those counters into `State` so that the status bar and the
|
|
115
|
+
# context-window indicator work for ChatGPT Plus/Pro users too.
|
|
116
|
+
def handle_response_completed(event)
|
|
117
|
+
usage = event.dig("response", "usage") || event["usage"]
|
|
118
|
+
return unless usage.is_a?(Hash)
|
|
119
|
+
|
|
120
|
+
@state.update_last_message_tokens(
|
|
121
|
+
model: @model,
|
|
122
|
+
input_tokens: usage["input_tokens"],
|
|
123
|
+
output_tokens: usage["output_tokens"],
|
|
124
|
+
thinking_tokens: usage.dig("output_tokens_details", "reasoning_tokens"),
|
|
125
|
+
cached_tokens: usage.dig("input_tokens_details", "cached_tokens")
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
110
129
|
def finalize_response(assistant_text)
|
|
111
130
|
@conversation_history << { role: "assistant", content: assistant_text } unless assistant_text.empty?
|
|
112
131
|
end
|
|
@@ -131,6 +150,7 @@ module RubyCoded
|
|
|
131
150
|
nil
|
|
132
151
|
end
|
|
133
152
|
end
|
|
153
|
+
# rubocop:enable Metrics/ModuleLength
|
|
134
154
|
end
|
|
135
155
|
end
|
|
136
156
|
end
|
|
@@ -6,23 +6,34 @@ module RubyCoded
|
|
|
6
6
|
# These models are not listed in RubyLLM.models because they use a
|
|
7
7
|
# different API (Responses API via chatgpt.com/backend-api).
|
|
8
8
|
module CodexModels
|
|
9
|
-
|
|
9
|
+
PROVIDER = :openai
|
|
10
|
+
|
|
11
|
+
CodexModel = Struct.new(:id, :display_name, :context_window, :max_output, :pro_only,
|
|
12
|
+
keyword_init: true) do
|
|
10
13
|
def to_s
|
|
11
14
|
id
|
|
12
15
|
end
|
|
16
|
+
|
|
17
|
+
def provider
|
|
18
|
+
PROVIDER
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def pro_only?
|
|
22
|
+
pro_only == true
|
|
23
|
+
end
|
|
13
24
|
end
|
|
14
25
|
|
|
15
26
|
MODELS = [
|
|
16
27
|
CodexModel.new(id: "gpt-5.4", display_name: "GPT 5.4 (Recommended)",
|
|
17
|
-
context_window: 272_000, max_output: 128_000),
|
|
28
|
+
context_window: 272_000, max_output: 128_000, pro_only: false),
|
|
18
29
|
CodexModel.new(id: "gpt-5.4-mini", display_name: "GPT 5.4 Mini",
|
|
19
|
-
context_window: 272_000, max_output: 128_000),
|
|
20
|
-
CodexModel.new(id: "gpt-5.3-codex-spark", display_name: "GPT 5.3 Codex Spark
|
|
21
|
-
context_window: 272_000, max_output: 128_000),
|
|
30
|
+
context_window: 272_000, max_output: 128_000, pro_only: false),
|
|
31
|
+
CodexModel.new(id: "gpt-5.3-codex-spark", display_name: "GPT 5.3 Codex Spark",
|
|
32
|
+
context_window: 272_000, max_output: 128_000, pro_only: true),
|
|
22
33
|
CodexModel.new(id: "gpt-5.2-codex", display_name: "GPT 5.2 Codex",
|
|
23
|
-
context_window: 272_000, max_output: 128_000),
|
|
34
|
+
context_window: 272_000, max_output: 128_000, pro_only: true),
|
|
24
35
|
CodexModel.new(id: "gpt-5.2", display_name: "GPT 5.2",
|
|
25
|
-
context_window: 272_000, max_output: 128_000)
|
|
36
|
+
context_window: 272_000, max_output: 128_000, pro_only: false)
|
|
26
37
|
].freeze
|
|
27
38
|
|
|
28
39
|
def self.all
|
|
@@ -36,6 +47,24 @@ module RubyCoded
|
|
|
36
47
|
def self.codex_model?(id)
|
|
37
48
|
MODELS.any? { |m| m.id == id }
|
|
38
49
|
end
|
|
50
|
+
|
|
51
|
+
def self.pro_only?(id)
|
|
52
|
+
model = find(id)
|
|
53
|
+
model ? model.pro_only? : false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
PRO_TIER_PLANS = %w[pro team enterprise business edu].freeze
|
|
57
|
+
|
|
58
|
+
def self.available_for_plan(plan)
|
|
59
|
+
normalized = plan.to_s.downcase
|
|
60
|
+
return MODELS if normalized.empty? || PRO_TIER_PLANS.any? { |p| normalized.include?(p) }
|
|
61
|
+
|
|
62
|
+
if normalized.include?("plus") || normalized.include?("free")
|
|
63
|
+
MODELS.reject(&:pro_only?)
|
|
64
|
+
else
|
|
65
|
+
MODELS
|
|
66
|
+
end
|
|
67
|
+
end
|
|
39
68
|
end
|
|
40
69
|
end
|
|
41
70
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCoded
|
|
4
|
+
module Chat
|
|
5
|
+
class CommandHandler
|
|
6
|
+
# Slash commands for managing custom markdown commands.
|
|
7
|
+
module CustomCommands
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def cmd_commands(rest)
|
|
11
|
+
case rest&.strip&.downcase
|
|
12
|
+
when "reload"
|
|
13
|
+
reload_commands
|
|
14
|
+
when "list"
|
|
15
|
+
list_commands
|
|
16
|
+
else
|
|
17
|
+
@state.add_message(:system, "Usage: /commands [reload|list]")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reload_commands
|
|
22
|
+
return missing_command_catalog unless @command_catalog
|
|
23
|
+
|
|
24
|
+
report = @command_catalog.reload!
|
|
25
|
+
@commands = build_command_map
|
|
26
|
+
@state.add_message(:system, format_reload_message(report))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def list_commands
|
|
30
|
+
return missing_command_catalog unless @command_catalog
|
|
31
|
+
|
|
32
|
+
commands = @command_catalog.definitions_for_source(:markdown)
|
|
33
|
+
return show_empty_custom_commands if commands.empty?
|
|
34
|
+
|
|
35
|
+
@state.add_message(:system, formatted_custom_commands(commands))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def missing_command_catalog
|
|
39
|
+
@state.add_message(:system, "Command catalog is not available.")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show_empty_custom_commands
|
|
43
|
+
@state.add_message(
|
|
44
|
+
:system,
|
|
45
|
+
"No custom commands loaded. Add markdown files under .ruby_coded/commands " \
|
|
46
|
+
"and run /commands reload."
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def formatted_custom_commands(commands)
|
|
51
|
+
lines = ["Custom commands:"]
|
|
52
|
+
commands.sort_by { |definition| definition.name.downcase }.each do |definition|
|
|
53
|
+
lines << formatted_command_line(definition)
|
|
54
|
+
end
|
|
55
|
+
lines.join("\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def format_reload_message(report)
|
|
59
|
+
message = reload_summary(report)
|
|
60
|
+
details = reload_details(report)
|
|
61
|
+
return message if details.empty?
|
|
62
|
+
|
|
63
|
+
"#{message}\n#{details.join("\n")}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def reload_summary(report)
|
|
67
|
+
"Commands reloaded. " \
|
|
68
|
+
"Added: #{report[:added]}, removed: #{report[:removed]}, " \
|
|
69
|
+
"total custom commands: #{report[:total]}, " \
|
|
70
|
+
"invalid files ignored: #{report[:invalid]}, " \
|
|
71
|
+
"conflicts ignored: #{report[:conflicts]}."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reload_details(report)
|
|
75
|
+
details = []
|
|
76
|
+
invalid_files = Array(report[:invalid_files])
|
|
77
|
+
conflict_commands = Array(report[:conflict_commands])
|
|
78
|
+
|
|
79
|
+
details << "Invalid files: #{invalid_files.join(", ")}" if invalid_files.any?
|
|
80
|
+
details << "Conflicting commands: #{conflict_commands.join(", ")}" if conflict_commands.any?
|
|
81
|
+
details
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def formatted_command_line(definition)
|
|
85
|
+
usage = definition.usage || definition.name
|
|
86
|
+
" #{usage.ljust(28)} #{definition.description}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../model_filter"
|
|
4
4
|
require_relative "../codex_models"
|
|
5
|
+
require_relative "../../auth/jwt_decoder"
|
|
5
6
|
|
|
6
7
|
module RubyCoded
|
|
7
8
|
module Chat
|
|
@@ -74,12 +75,18 @@ module RubyCoded
|
|
|
74
75
|
|
|
75
76
|
def models_for_provider(name, creds)
|
|
76
77
|
if name == :openai && creds["auth_method"] == "oauth"
|
|
77
|
-
|
|
78
|
+
codex_models_for_plan(creds)
|
|
78
79
|
else
|
|
79
80
|
RubyLLM.models.by_provider(name).chat_models.to_a
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
def codex_models_for_plan(creds)
|
|
85
|
+
token = creds["access_token"]
|
|
86
|
+
plan = token ? Auth::JWTDecoder.extract_plan_type(token) : nil
|
|
87
|
+
CodexModels.available_for_plan(plan)
|
|
88
|
+
end
|
|
89
|
+
|
|
83
90
|
def fetch_chat_models
|
|
84
91
|
RubyLLM.models.chat_models.to_a
|
|
85
92
|
rescue StandardError
|