Skip to content

Introduce config.ai_call: pluggable AI surface (messages-array)#122

Merged
HamptonMakes merged 2 commits into
hampton/coplan-23/remove-review-botsfrom
hampton/coplan-23/ai-call-abstraction
Jun 2, 2026
Merged

Introduce config.ai_call: pluggable AI surface (messages-array)#122
HamptonMakes merged 2 commits into
hampton/coplan-23/remove-review-botsfrom
hampton/coplan-23/ai-call-abstraction

Conversation

@HamptonMakes
Copy link
Copy Markdown
Collaborator

Why

Replaces the engine's hard-coded CoPlan::AiProviders::OpenAi.call plumbing with a host-supplied callable so deployments can swap in any backend — raw OpenAI, an internal LLM gateway like Square's Gondola, an Anthropic client, a test stub, etc. — without touching engine code.

This is the engine-side prerequisite for wiring CoPlan-on-Square through Gondola (vs. uploading a raw OPENAI_API_KEY to Keywhiz, which was the path the now-closed coplan-square#75 took).

📚 Stacked on #121 (remove-review-bots) — review that one first. This PR's diff above the base is small; if you base-compare against main you'll see the review-bot deletions too.

What the engine now exposes

CoPlan::Ai.call(messages: [
  { role: :system, content: "You are concise." },
  { role: :user,   content: "Hi" },
])

# …with sugar for the 90% case:
CoPlan::Ai.call(system: "You are concise.", user: "Hi")

The messages-array shape is the canonical wire format every chat-style LLM API speaks (OpenAI Chat Completions, Anthropic Messages, Gondola, Bedrock), so the host's lambda passes it straight through with zero translation. thread_id, streaming, tool calls, etc. are deliberately not part of this surface — if/when we need them they get their own slots (config.ai_chat, config.ai_stream, etc.). Premature to design for it.

What the host wires

# config/initializers/coplan.rb (Square deployment)
config.ai_call = ->(messages:) {
  GondolaProvider.call(messages: messages, model: "gpt-4o")
}

Model choice and deployment policy (which provider, project, rate limits, observability) live inside the callable — not on the engine's API surface. The engine never sees an API key or a model name.

Built-in default

When ENV["OPENAI_API_KEY"] is present, Configuration#initialize auto-wires CoPlan::AiProviders::OpenAi — so a standalone deployment of the engine works with zero extra config. The OpenAI plugin now accepts a messages array and honors ENV["OPENAI_MODEL"] (default gpt-4o) for per-deployment model tuning without code changes.

Removed

Surface Was Now
Configuration#ai_api_key / #ai_base_url / #ai_model Public engine accessors Removed — provider/key/model live inside the host's lambda
Host's config.ai_api_key = ... line Required for OpenAI to work Removed — auto-wire handles it when ENV var is set

Added

  • Configuration#ai_call with full docstring + #ai_call_configured? predicate
  • CoPlan::Ai::NoProviderError (subclass of Ai::Error) so jobs can distinguish "not configured" from "call failed"
  • Convenience sugar in CoPlan::Ai.call for the single-shot (system:, user:) case

Errors

Any exception raised inside the host's lambda is wrapped in CoPlan::Ai::Error, so existing call sites (SummarizePlanJob.discard_on CoPlan::Ai::Error) continue to work unchanged without coupling to the provider. ArgumentError (caller bug — empty messages) propagates normally so it isn't silently swallowed.

Verification

Check Result
bundle exec rspec ✅ 868 examples, 0 failures
Grep for system_prompt / user_content / ai_api_key / ai_base_url ✅ clean (only the historical coplan_plan_versions.ai_model column remains; out of scope to drop here)

Follow-ups (separate PRs)

  1. Cut a coplan-engine gem release after merge.
  2. Bump coplan-engine in squareup/coplan-square, copy in the engine migration, and wire config.ai_call to a Gondola-backed Ruby helper (which means registering CoPlan's model in Gondola + adding gondola-ski as a Registry dep on coplan-square).

🤖 Generated with Amp

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6c4cfbcde

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread engine/lib/coplan/configuration.rb Outdated
Comment on lines +86 to +87
@ai_call = if ENV["OPENAI_API_KEY"].present?
->(messages:) { CoPlan::AiProviders::OpenAi.call(messages: messages) }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include credentials when auto-wiring OpenAI

When a deployment stores its OpenAI key in Rails credentials rather than ENV, this leaves ai_call nil because the default provider is only installed when OPENAI_API_KEY is present. The built-in OpenAi#api_key still supports Rails.application.credentials.dig(:openai, :api_key), and this commit removed the host initializer's config.ai_api_key assignment, so credential-backed deployments now hit CoPlan::Ai::NoProviderError and discard summary jobs despite having a valid key. Please include credentials in the auto-wire check or make the default provider lazy.

Useful? React with 👍 / 👎.

@@ -1,7 +1,6 @@
module CoPlan
class Configuration
attr_accessor :authenticate, :api_authenticate, :sign_in_path
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep documented AI setters from breaking host boot

The in-repo host guide still instructs hosts to set config.ai_api_key / config.ai_model and config.ai_base_url (docs/HOST_APP_GUIDE.md lines 66-68 and 140-142). After these accessors are removed, any host initializer following the current docs or upgrading before migrating to config.ai_call will raise NoMethodError during boot, before the app can start. Either keep deprecated setters as compatibility shims or update the documented migration path in this change.

Useful? React with 👍 / 👎.

@HamptonMakes HamptonMakes force-pushed the hampton/coplan-23/remove-review-bots branch from 7aff15a to c7e2124 Compare June 2, 2026 21:41
Replaces the engine's hard-coded \`CoPlan::AiProviders::OpenAi.call\`
plumbing with a host-supplied callable so deployments can swap in any
backend (raw OpenAI, an internal LLM gateway like Square's Gondola, a
test stub, etc.) without touching engine code.

  CoPlan::Ai.call(messages: [
    { role: :system, content: "You are concise." },
    { role: :user,   content: "Hi" },
  ])

  # …with sugar for the 90% case:
  CoPlan::Ai.call(system: "You are concise.", user: "Hi")

The messages-array shape is the canonical wire format every chat-style
LLM API speaks (OpenAI chat completions, Anthropic Messages, Gondola,
Bedrock), so the host's lambda passes it straight through with zero
translation.

  # config/initializers/coplan.rb
  config.ai_call = ->(messages:) {
    GondolaProvider.call(messages: messages, model: "gpt-4o")
  }

Model choice and deployment policy (which provider, project, rate
limits, observability) live inside the callable — not on the engine's
API surface. The engine never sees an API key or a model name.

When \`ENV["OPENAI_API_KEY"]\` is present, Configuration auto-wires
\`CoPlan::AiProviders::OpenAi\` so a standalone deployment works with
zero config. The OpenAI plugin now accepts a messages array and honors
\`ENV["OPENAI_MODEL"]\` (default gpt-4o) for per-deployment model
tuning without code changes.

- \`Configuration#ai_api_key\`, \`#ai_base_url\`, \`#ai_model\` accessors
  (deployment concerns now live inside the provider lambda)
- \`coplan-square\`-style \`config.ai_api_key = ...\` lines in the host
  initializer (auto-wire handles it)

- \`Configuration#ai_call\` (with docstring) + \`#ai_call_configured?\`
  predicate
- \`CoPlan::Ai::NoProviderError\` (subclass of \`Ai::Error\`) for jobs
  that want to distinguish "not configured" from "call failed"

Any exception raised inside the host's lambda is wrapped in
\`CoPlan::Ai::Error\`, so existing call sites
(\`SummarizePlanJob.discard_on CoPlan::Ai::Error\`) continue to work
unchanged without coupling to the provider.

Tests: 868 examples, 0 failures.

Stacked on #121 (review-bot removal).

🤖 Generated with [Amp](https://ampcode.com)

Amp-Thread-ID: https://ampcode.com/threads/T-019e84ca-1bd1-7435-acbc-73b00095d896
Co-authored-by: Amp <amp@ampcode.com>
@HamptonMakes HamptonMakes force-pushed the hampton/coplan-23/ai-call-abstraction branch from e6c4cfb to edb377b Compare June 2, 2026 21:42
- Auto-wire OpenAI default unconditionally (lazy). The provider already
  resolves its key from Rails credentials -> ENV and raises
  CoPlan::Ai::Error at call time when neither is set, so credential-only
  deployments now wire up correctly instead of silently leaving ai_call
  nil. AI jobs still discard cleanly.
- Drop unused ai_call_configured? helper (no longer meaningful now that
  the default is always wired; nothing called it).
- Update docs/HOST_APP_GUIDE.md to remove the obsolete ai_api_key /
  ai_base_url / ai_model setters and document config.ai_call with the
  built-in OpenAI auto-wire behavior, so hosts following the guide
  don't NoMethodError on boot.

Amp-Thread-ID: https://ampcode.com/threads/T-019e84ca-1bd1-7435-acbc-73b00095d896
Co-authored-by: Amp <amp@ampcode.com>
@HamptonMakes
Copy link
Copy Markdown
Collaborator Author

Addressed both Codex notes in 457a42a:

  1. Credential-backed auto-wire — made the default ai_call lazy/unconditional. The OpenAI provider already resolves its key from Rails credentials (openai.api_key) → ENV["OPENAI_API_KEY"] and raises CoPlan::Ai::Error at call time if neither is set, so credential-only deployments now wire up correctly instead of silently leaving ai_call nil. AI jobs still discard_on cleanly.
  2. HOST_APP_GUIDE — removed the obsolete ai_api_key / ai_base_url / ai_model setters from the guide and documented config.ai_call plus the built-in OpenAI auto-wire behavior, so hosts following the docs don't NoMethodError on boot.

Also dropped the now-unused ai_call_configured? helper (always true once the default is wired, and nothing called it). 898 specs green.

@HamptonMakes HamptonMakes merged commit 2d286b3 into hampton/coplan-23/remove-review-bots Jun 2, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant