Revert "Refactor application workflows and update test coverage"

This reverts commit 565e049f89.
This commit is contained in:
Juan José Mata
2026-07-23 13:39:46 -07:00
parent 565e049f89
commit c6eb7cdeed
15 changed files with 902 additions and 1166 deletions

View File

@@ -25,12 +25,6 @@ OPENAI_ACCESS_TOKEN=
OPENAI_MODEL=
OPENAI_URI_BASE=
# Alternative: use a ChatGPT-backed Codex OAuth token for subscription inference.
# OAuth takes precedence over OPENAI_ACCESS_TOKEN and uses the Codex Responses endpoint.
# OPENAI_ACCOUNT_ID is optional when it can be read from the token's JWT claims.
# OPENAI_OAUTH_TOKEN=
# OPENAI_ACCOUNT_ID=
# Optional: LLM token budget (applies to chat, auto-categorize, merchant detection, PDF processing).
# Lower these for small-context local models (Ollama, LM Studio, LocalAI).
# For larger local models, raise the context window to match the model you actually run.

View File

@@ -32,12 +32,6 @@ OPENAI_ACCESS_TOKEN =
OPENAI_URI_BASE =
OPENAI_MODEL =
# Alternative: ChatGPT-backed Codex OAuth subscription inference.
# OAuth takes precedence over OPENAI_ACCESS_TOKEN and uses the Codex Responses endpoint.
# OPENAI_ACCOUNT_ID is optional when it can be read from the token's JWT claims.
# OPENAI_OAUTH_TOKEN =
# OPENAI_ACCOUNT_ID =
# LLM token budget. Applies to ALL outbound LLM calls: chat history,
# auto-categorize, merchant detection, provider enhancer, PDF processing.
# Defaults to Ollama's historical 2048-token baseline so small local models

View File

@@ -152,19 +152,6 @@ class Settings::HostingsController < ApplicationController
end
end
if hosting_params.key?(:openai_oauth_token)
token_param = hosting_params[:openai_oauth_token].to_s.strip
unless token_param.blank? || token_param == "********"
Setting.openai_oauth_token = token_param
inferred_account_id = Provider::Openai.oauth_account_id(token_param)
Setting.openai_oauth_account_id = inferred_account_id if inferred_account_id.present? && Setting.openai_oauth_account_id.blank?
end
end
if hosting_params.key?(:openai_oauth_account_id)
Setting.openai_oauth_account_id = hosting_params[:openai_oauth_account_id].to_s.strip.presence
end
# Validate OpenAI configuration before updating
if hosting_params.key?(:openai_uri_base) || hosting_params.key?(:openai_model)
Setting.validate_openai_config!(
@@ -293,7 +280,7 @@ class Settings::HostingsController < ApplicationController
private
def hosting_params
return ActionController::Parameters.new unless params.key?(:setting)
params.require(:setting).permit(:onboarding_state, :require_email_confirmation, :invite_only_default_family_id, :brand_fetch_client_id, :brand_fetch_high_res_logos, :twelve_data_api_key, :tiingo_api_key, :eodhd_api_key, :alpha_vantage_api_key, :tinkoff_invest_api_key, :openai_access_token, :openai_oauth_token, :openai_oauth_account_id, :openai_uri_base, :openai_model, :openai_json_mode, :anthropic_access_token, :anthropic_base_url, :anthropic_model, :llm_provider, :llm_context_window, :llm_max_response_tokens, :llm_max_items_per_call, :exchange_rate_provider, :securities_provider, :syncs_include_pending, :auto_sync_enabled, :auto_sync_time, :external_assistant_url, :external_assistant_token, :external_assistant_agent_id, securities_providers: [])
params.require(:setting).permit(:onboarding_state, :require_email_confirmation, :invite_only_default_family_id, :brand_fetch_client_id, :brand_fetch_high_res_logos, :twelve_data_api_key, :tiingo_api_key, :eodhd_api_key, :alpha_vantage_api_key, :tinkoff_invest_api_key, :openai_access_token, :openai_uri_base, :openai_model, :openai_json_mode, :anthropic_access_token, :anthropic_base_url, :anthropic_model, :llm_provider, :llm_context_window, :llm_max_response_tokens, :llm_max_items_per_call, :exchange_rate_provider, :securities_provider, :syncs_include_pending, :auto_sync_enabled, :auto_sync_time, :external_assistant_url, :external_assistant_token, :external_assistant_agent_id, securities_providers: [])
end
def update_assistant_type

View File

@@ -1,6 +1,4 @@
class Provider::Github
CACHE_TTL = 2.hours
attr_reader :name, :owner, :branch, :client
def initialize
@@ -19,7 +17,7 @@ class Provider::Github
def fetch_latest_release_notes
begin
Rails.cache.fetch(release_notes_cache_key, expires_in: CACHE_TTL) do
Rails.cache.fetch("latest_github_release_notes", expires_in: 2.hours) do
release = client.releases(repo).first
if release
{
@@ -44,8 +42,4 @@ class Provider::Github
def repo
"#{owner}/#{name}"
end
def release_notes_cache_key
"latest_github_release_notes/#{repo}"
end
end

View File

@@ -1,5 +1,3 @@
require "base64"
class Provider::Openai < Provider
include LlmConcept
@@ -7,65 +5,32 @@ class Provider::Openai < Provider
Error = Class.new(Provider::Error)
DEFAULT_MODEL = "gpt-4.1".freeze
CODEX_DEFAULT_MODEL = "gpt-5.6".freeze
CODEX_URI_BASE = "https://chatgpt.com/backend-api/codex".freeze
CODEX_ORIGINATOR = "codex_cli_rs".freeze
SUPPORTED_MODELS = %w[gpt-4 gpt-5 o1 o3].freeze
VISION_CAPABLE_MODEL_PREFIXES = %w[gpt-4o gpt-4-turbo gpt-4.1 gpt-5 o1 o3].freeze
# Returns the effective model that would be used by the provider.
# Priority: explicit ENV > Setting > DEFAULT_MODEL.
def self.effective_model
configured_model = ENV.fetch("OPENAI_MODEL") { Setting.openai_model }.presence
configured_model || (oauth_configured? ? CODEX_DEFAULT_MODEL : DEFAULT_MODEL)
ENV.fetch("OPENAI_MODEL") { Setting.openai_model }.presence || DEFAULT_MODEL
end
def self.configured?
oauth_configured? || ENV["OPENAI_ACCESS_TOKEN"].present? || Setting.openai_access_token.present?
ENV["OPENAI_ACCESS_TOKEN"].present? || Setting.openai_access_token.present?
end
def self.oauth_configured?
ENV["OPENAI_OAUTH_TOKEN"].present? || Setting.openai_oauth_token.present?
end
def self.oauth_account_id(access_token)
payload = access_token.to_s.split(".", 3).second
return if payload.blank?
payload = payload.ljust((payload.length + 3) / 4 * 4, "=")
claims = JSON.parse(Base64.urlsafe_decode64(payload))
claims["chatgpt_account_id"].presence ||
claims.dig("https://api.openai.com/auth", "chatgpt_account_id").presence
rescue ArgumentError, JSON::ParserError
nil
end
def initialize(access_token, uri_base: nil, model: nil, oauth: false, account_id: nil)
def initialize(access_token, uri_base: nil, model: nil)
client_options = { access_token: access_token }
@oauth = oauth
llm_uri_base = oauth? ? CODEX_URI_BASE : uri_base.presence
llm_uri_base = uri_base.presence
llm_model = model.presence
client_options[:uri_base] = llm_uri_base if llm_uri_base.present?
client_options[:request_timeout] = ENV.fetch("OPENAI_REQUEST_TIMEOUT", 60).to_i
if oauth?
oauth_account_id = account_id.presence || self.class.oauth_account_id(access_token)
if oauth_account_id.blank?
raise Error, "ChatGPT account ID is required when using a Codex OAuth token"
end
client_options[:extra_headers] = {
"ChatGPT-Account-ID" => oauth_account_id,
"originator" => CODEX_ORIGINATOR
}
end
@client = ::OpenAI::Client.new(**client_options)
@uri_base = llm_uri_base
if custom_provider? && llm_model.blank?
raise Error, "Model is required when using a custom OpenAIcompatible provider"
end
@default_model = llm_model.presence || (oauth? ? CODEX_DEFAULT_MODEL : self.class.effective_model)
@default_model = llm_model.presence || self.class.effective_model
end
def supports_model?(model)
@@ -88,8 +53,6 @@ class Provider::Openai < Provider
end
def provider_name
return "OpenAI (Codex subscription)" if oauth?
custom_provider? ? "Custom OpenAI-compatible (#{@uri_base})" : "OpenAI"
end
@@ -102,11 +65,7 @@ class Provider::Openai < Provider
end
def custom_provider?
@uri_base.present? && !oauth?
end
def oauth?
@oauth
@uri_base.present?
end
# Token-budget knobs. Precedence: ENV > Setting > default. Defaults match

View File

@@ -77,19 +77,13 @@ class Provider::Registry
end
def openai
oauth_token = ENV["OPENAI_OAUTH_TOKEN"].presence || Setting.openai_oauth_token
access_token = oauth_token || ENV["OPENAI_ACCESS_TOKEN"].presence || Setting.openai_access_token
access_token = ENV["OPENAI_ACCESS_TOKEN"].presence || Setting.openai_access_token
return nil unless access_token.present?
uri_base = ENV["OPENAI_URI_BASE"].presence || Setting.openai_uri_base
model = ENV["OPENAI_MODEL"].presence || Setting.openai_model
if oauth_token.present?
account_id = ENV["OPENAI_ACCOUNT_ID"].presence || Setting.openai_oauth_account_id
return Provider::Openai.new(access_token, model: model, oauth: true, account_id: account_id)
end
if uri_base.present? && model.blank?
Rails.logger.error("Custom OpenAI provider configured without a model; please set OPENAI_MODEL or Setting.openai_model")
return nil

View File

@@ -7,8 +7,6 @@ class Setting < RailsSettings::Base
# Third-party API keys
field :twelve_data_api_key, type: :string, default: ENV["TWELVE_DATA_API_KEY"]
field :openai_access_token, type: :string, default: ENV["OPENAI_ACCESS_TOKEN"]
field :openai_oauth_token, type: :string, default: ENV["OPENAI_OAUTH_TOKEN"]
field :openai_oauth_account_id, type: :string, default: ENV["OPENAI_ACCOUNT_ID"]
field :openai_uri_base, type: :string, default: ENV["OPENAI_URI_BASE"]
field :openai_model, type: :string, default: ENV["OPENAI_MODEL"]
field :openai_json_mode, type: :string, default: ENV["LLM_JSON_MODE"]
@@ -78,7 +76,6 @@ class Setting < RailsSettings::Base
alpha_vantage_api_key
tinkoff_invest_api_key
openai_access_token
openai_oauth_token
anthropic_access_token
external_assistant_token
].freeze

View File

@@ -1,7 +1,7 @@
<div class="space-y-4">
<div>
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
<% if ENV["OPENAI_ACCESS_TOKEN"].present? || ENV["OPENAI_OAUTH_TOKEN"].present? %>
<% if ENV["OPENAI_ACCESS_TOKEN"].present? %>
<p class="text-sm text-secondary"><%= t(".env_configured_message") %></p>
<% else %>
<p class="text-secondary text-sm mb-4"><%= t(".description") %></p>
@@ -27,34 +27,6 @@
disabled: ENV["OPENAI_ACCESS_TOKEN"].present?,
data: { "auto-submit-form-target": "auto" } %>
<div class="pt-4 border-t border-secondary">
<h3 class="font-medium mb-1"><%= t(".oauth_heading") %></h3>
<p class="text-xs text-secondary mb-3"><%= t(".oauth_description") %></p>
<%= form.password_field :openai_oauth_token,
label: t(".oauth_token_label"),
placeholder: t(".oauth_token_placeholder"),
value: (Setting.openai_oauth_token.present? ? "********" : nil),
autocomplete: "off",
autocapitalize: "none",
spellcheck: "false",
inputmode: "text",
disabled: ENV["OPENAI_OAUTH_TOKEN"].present?,
data: { "auto-submit-form-target": "auto" } %>
<%= form.text_field :openai_oauth_account_id,
label: t(".oauth_account_id_label"),
placeholder: t(".oauth_account_id_placeholder"),
value: Setting.openai_oauth_account_id,
autocomplete: "off",
autocapitalize: "none",
spellcheck: "false",
inputmode: "text",
disabled: ENV["OPENAI_ACCOUNT_ID"].present?,
data: { "auto-submit-form-target": "auto" } %>
<p class="text-xs text-secondary mt-1"><%= t(".oauth_account_id_help") %></p>
</div>
<%= form.text_field :openai_uri_base,
label: t(".uri_base_label"),
placeholder: t(".uri_base_placeholder"),

View File

@@ -118,17 +118,10 @@ en:
model_placeholder: "claude-sonnet-4-6 (default)"
model_help: Used for chat and PDF processing. Batch operations (categorize, merchant detection) default to Haiku for cost.
openai_settings:
description: Enter an API key, a Codex OAuth token, or configure a custom OpenAI-compatible provider
description: Enter the access token and optionally configure a custom OpenAI-compatible provider
env_configured_message: Successfully configured through environment variables.
access_token_label: API Key
access_token_placeholder: Enter your OpenAI-compatible API key here
oauth_heading: Codex Subscription (OAuth)
oauth_description: Use a ChatGPT-backed Codex OAuth access token instead of API billing. OAuth takes precedence when both credential types are configured. Tokens expire and must be replaced after you sign in again.
oauth_token_label: OAuth Access Token
oauth_token_placeholder: Enter your Codex OAuth access token here
oauth_account_id_label: ChatGPT Account ID (Optional)
oauth_account_id_placeholder: Account ID from your Codex login
oauth_account_id_help: Usually read from the token automatically. Enter it when your token does not contain the ChatGPT account claim.
access_token_label: Access Token
access_token_placeholder: Enter your access token here
uri_base_label: API Base URL (Optional)
uri_base_placeholder: "https://api.openai.com/v1 (default)"
model_label: Model (Optional)

1777
db/schema.rb generated

File diff suppressed because it is too large Load Diff

View File

@@ -48,22 +48,6 @@ The easiest way to get started with AI features in Sure is to use OpenAI:
That's it! Sure will use OpenAI's with a default model (currently `gpt-4.1`) for all AI operations.
## Alternative: Codex Subscription OAuth
Sure can send Responses API inference through a ChatGPT-backed Codex subscription instead of billing an OpenAI Platform API key:
```bash
OPENAI_OAUTH_TOKEN=your-codex-oauth-access-token
# Optional when the account ID cannot be read from the token:
OPENAI_ACCOUNT_ID=your-chatgpt-account-id
# Optional; defaults to the current Codex default:
OPENAI_MODEL=gpt-5.6
```
When `OPENAI_OAUTH_TOKEN` is present, it takes precedence over `OPENAI_ACCESS_TOKEN`. Sure sends requests to the Codex Responses endpoint and adds the ChatGPT account header required by subscription authentication. This credential is only used for LLM inference; it is not treated as a general OpenAI Platform API key for hosted vector stores or other Platform APIs.
OAuth access tokens expire. Sign in again with an official Codex client and replace the configured token when that happens. Store the token like a password and never commit it to an environment file.
## Local vs. Cloud Inference
### Cloud Inference (Recommended for Most Users)

View File

@@ -25,7 +25,7 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
teardown do
# These tests persist global Setting.* values; reset them so state can't
# leak into later (order-dependent) tests.
%i[openai_oauth_token openai_oauth_account_id anthropic_access_token anthropic_base_url anthropic_model llm_provider].each do |key|
%i[anthropic_access_token anthropic_base_url anthropic_model llm_provider].each do |key|
Setting.public_send("#{key}=", nil)
end
end
@@ -156,43 +156,6 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
end
end
test "can update Codex OAuth credentials when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: {
setting: {
openai_oauth_token: "oauth-token",
openai_oauth_account_id: "account-123"
}
}
assert_equal "oauth-token", Setting.openai_oauth_token
assert_equal "account-123", Setting.openai_oauth_account_id
end
end
test "infers ChatGPT account ID from Codex OAuth token" do
payload = Base64.urlsafe_encode64({
"https://api.openai.com/auth" => { "chatgpt_account_id" => "account-from-token" }
}.to_json, padding: false)
token = "header.#{payload}.signature"
with_self_hosting do
patch settings_hosting_url, params: { setting: { openai_oauth_token: token } }
assert_equal "account-from-token", Setting.openai_oauth_account_id
end
end
test "ignores redacted Codex OAuth token placeholder" do
with_self_hosting do
Setting.openai_oauth_token = "previous-token"
patch settings_hosting_url, params: { setting: { openai_oauth_token: "********" } }
assert_equal "previous-token", Setting.openai_oauth_token
end
end
test "can update anthropic access token when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { anthropic_access_token: "fake-anthropic-key-for-tests" } }

View File

@@ -1,16 +0,0 @@
require "test_helper"
class Provider::GithubTest < ActiveSupport::TestCase
test "release notes cache is scoped to the configured repository" do
provider = Provider::Github.new
Rails.cache.expects(:fetch)
.with("latest_github_release_notes/we-promise/sure", expires_in: Provider::Github::CACHE_TTL)
.yields
.returns(nil)
provider.client.expects(:releases).with("we-promise/sure").returns([])
assert_nil provider.fetch_latest_release_notes
end
end

View File

@@ -8,51 +8,6 @@ class Provider::OpenaiTest < ActiveSupport::TestCase
@subject_model = "gpt-4.1"
end
test "decodes ChatGPT account ID from OAuth token claims" do
token = oauth_token_for("account-123")
assert_equal "account-123", Provider::Openai.oauth_account_id(token)
end
test "returns nil when OAuth token claims cannot be decoded" do
assert_nil Provider::Openai.oauth_account_id("not-a-jwt")
end
test "configures Codex subscription endpoint and headers for OAuth" do
provider = Provider::Openai.new(oauth_token_for("account-123"), oauth: true)
client = provider.send(:client)
assert provider.oauth?
refute provider.custom_provider?
assert provider.supports_responses_endpoint?
assert_equal Provider::Openai::CODEX_URI_BASE, client.uri_base
assert_equal "account-123", client.extra_headers["ChatGPT-Account-ID"]
assert_equal Provider::Openai::CODEX_ORIGINATOR, client.extra_headers["originator"]
assert_equal "OpenAI (Codex subscription)", provider.provider_name
end
test "uses explicit account ID for opaque OAuth tokens" do
provider = Provider::Openai.new("opaque-token", oauth: true, account_id: "account-456")
assert_equal "account-456", provider.send(:client).extra_headers["ChatGPT-Account-ID"]
end
test "requires an account ID for opaque OAuth tokens" do
error = assert_raises(Provider::Openai::Error) do
Provider::Openai.new("opaque-token", oauth: true)
end
assert_match(/ChatGPT account ID is required/, error.message)
end
test "uses Codex default model when OAuth is configured" do
ClimateControl.modify("OPENAI_OAUTH_TOKEN" => oauth_token_for("account-123"), "OPENAI_MODEL" => nil) do
Setting.stubs(:openai_model).returns(nil)
assert_equal Provider::Openai::CODEX_DEFAULT_MODEL, Provider::Openai.effective_model
end
end
test "openai errors are automatically raised" do
VCR.use_cassette("openai/chat/error") do
response = @openai.chat_response("Test", model: "invalid-model-that-will-trigger-api-error")
@@ -549,14 +504,4 @@ class Provider::OpenaiTest < ActiveSupport::TestCase
config.build_input(prompt: "hi", messages: [ { role: "user", content: "old" } ])
end
end
private
def oauth_token_for(account_id)
header = Base64.urlsafe_encode64({ alg: "none", typ: "JWT" }.to_json, padding: false)
payload = Base64.urlsafe_encode64({
"https://api.openai.com/auth" => { "chatgpt_account_id" => account_id }
}.to_json, padding: false)
"#{header}.#{payload}.signature"
end
end

View File

@@ -5,12 +5,10 @@ class Provider::RegistryTest < ActiveSupport::TestCase
# Ensure no LLM provider is configured
ClimateControl.modify(
"OPENAI_ACCESS_TOKEN" => nil,
"OPENAI_OAUTH_TOKEN" => nil,
"ANTHROPIC_ACCESS_TOKEN" => nil,
"ANTHROPIC_API_KEY" => nil
) do
Setting.stubs(:openai_access_token).returns(nil)
Setting.stubs(:openai_oauth_token).returns(nil)
Setting.stubs(:anthropic_access_token).returns(nil)
registry = Provider::Registry.for_concept(:llm)
@@ -42,9 +40,8 @@ class Provider::RegistryTest < ActiveSupport::TestCase
test "get_provider returns nil when provider not configured" do
# Ensure OpenAI is not configured
ClimateControl.modify("OPENAI_ACCESS_TOKEN" => nil, "OPENAI_OAUTH_TOKEN" => nil) do
ClimateControl.modify("OPENAI_ACCESS_TOKEN" => nil) do
Setting.stubs(:openai_access_token).returns(nil)
Setting.stubs(:openai_oauth_token).returns(nil)
registry = Provider::Registry.for_concept(:llm)
@@ -96,12 +93,10 @@ class Provider::RegistryTest < ActiveSupport::TestCase
# Use stub_env helper which properly stubs ENV access
ClimateControl.modify(
"OPENAI_ACCESS_TOKEN" => "",
"OPENAI_OAUTH_TOKEN" => "",
"OPENAI_URI_BASE" => "",
"OPENAI_MODEL" => ""
) do
Setting.stubs(:openai_access_token).returns("test-token-from-setting")
Setting.stubs(:openai_oauth_token).returns(nil)
Setting.stubs(:openai_uri_base).returns(nil)
Setting.stubs(:openai_model).returns(nil)
@@ -113,22 +108,6 @@ class Provider::RegistryTest < ActiveSupport::TestCase
end
end
test "openai provider uses Codex OAuth credentials when configured" do
ClimateControl.modify(
"OPENAI_OAUTH_TOKEN" => "oauth-token",
"OPENAI_ACCOUNT_ID" => "account-123",
"OPENAI_ACCESS_TOKEN" => "api-key",
"OPENAI_MODEL" => "gpt-5.6"
) do
provider = Provider::Registry.get_provider(:openai)
assert provider.oauth?
assert_equal "account-123", provider.send(:client).extra_headers["ChatGPT-Account-ID"]
assert_equal Provider::Openai::CODEX_URI_BASE, provider.send(:client).uri_base
end
end
test "preferred_llm_provider returns openai by default" do
openai = mock("openai")
Provider::Registry.stubs(:openai).returns(openai)