Resolve duas camadas de problema identificadas em teste end-to-end: 1. Embeddings falhavam com HTTP 404 (/codex/v1/embeddings não existe). Solução: Captain::Llm::EmbeddingService sempre usa OpenAI tradicional via Llm::Config.with_api_key(legacy_settings). ProviderConfig expõe legacy_openai_settings pra isso. 2. Servidor Codex ocasionalmente responde com response.failed + code=server_error (instabilidade transitória). Client agora retenta até 2x com backoff exponencial (0.5s, 1.5s) em erros retryable: HTTP 5xx, server_error no response.failed, ou stream inacabado. Outras correções nesta etapa: - Scenario#agent_model: em modo Codex, ignora CAPTAIN_OPEN_AI_MODEL_SCENARIO (que pode ter gpt-4o legado) e usa ProviderConfig.model. - ExtractionService/ContradictionCheckerService/TranslateQueryService: trocam constantes hardcoded gpt-4o-mini/gpt-4.1-nano por ProviderConfig.light_model (respeitando o provider ativo). - ProviderConfig.DEFAULT_CODEX_MODEL agora é gpt-5.2 (reconhecido pelo RubyLLM; gpt-5.4 não está no catalog do gem). Validado ponta-a-ponta: WhatsApp → Chatwoot → Jasmine → handoff Daniela → faq_lookup com embedding OK → resposta com preços corretos. Docs em docs/captain-codex-oauth.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
54 lines
1.5 KiB
Ruby
54 lines
1.5 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: captain_codex_credentials
|
|
#
|
|
# id :bigint not null, primary key
|
|
# access_token :text not null
|
|
# chatgpt_plan_type :string
|
|
# email :string
|
|
# expires_at :datetime not null
|
|
# last_refresh_at :datetime
|
|
# refresh_token :text not null
|
|
# status :string default("active"), not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# chatgpt_account_id :string
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_captain_codex_credentials_on_expires_at (expires_at)
|
|
# index_captain_codex_credentials_on_status (status)
|
|
#
|
|
class Captain::CodexCredential < ApplicationRecord
|
|
self.table_name = 'captain_codex_credentials'
|
|
|
|
STATUSES = %w[active expired revoked].freeze
|
|
|
|
encrypts :access_token
|
|
encrypts :refresh_token
|
|
|
|
validates :access_token, presence: true
|
|
validates :refresh_token, presence: true
|
|
validates :expires_at, presence: true
|
|
validates :status, inclusion: { in: STATUSES }
|
|
|
|
scope :active, -> { where(status: 'active') }
|
|
scope :expiring_soon, ->(skew = 5.minutes) { where(expires_at: ..(Time.current + skew)) }
|
|
|
|
def self.current
|
|
active.order(updated_at: :desc).first
|
|
end
|
|
|
|
def needs_refresh?(skew_seconds: 120)
|
|
expires_at.nil? || expires_at <= Time.current + skew_seconds.seconds
|
|
end
|
|
|
|
def expired?
|
|
expires_at <= Time.current
|
|
end
|
|
|
|
def revoke!
|
|
update!(status: 'revoked')
|
|
end
|
|
end
|