{"slug": "how-to-migrate-from-langchaingo-to-genkit-go", "title": "How to Migrate from LangChainGo to Genkit Go", "summary": "A developer has published a migration guide for moving Go AI services from LangChainGo to Google's Genkit Go, citing three unfixed defects in the abandoned library: a hardcoded default model (gemini-2.0-flash) that Google has retired, a Google provider built on the end-of-life github.com/google/generative-ai-go SDK, and broken streaming on every current Gemini model. The guide notes that LangChainGo auto-enables streaming whenever a callbacks handler is attached, meaning agents and instrumentation cannot coexist on Gemini today, and provides a concept-by-concept translation table with before-and-after code for the migration.", "body_md": "If you built a Go AI service in the last two years, there is a good chance it runs on **[LangChainGo](https://github.com/tmc/langchaingo)**. It was the obvious choice: the name was familiar, the concepts came straight from Python, and it had the biggest provider catalog in the Go ecosystem.\n\nThat choice has aged badly. LangChainGo's last release was `v0.1.14` in October 2025, its last commit to `main` was January 2026, and it is still on `0.1.x` after three and a half years. Meanwhile the ecosystem moved: models were retired, SDKs were replaced, and APIs changed shape. LangChainGo did not move with them, and parts of it are broken today as a result.\n\n**[Genkit Go](https://genkit.dev/docs/go/get-started/)** is where that workload belongs now. It is GA, it is on `v1.13.1`, and it is actively maintained by Google.\n\nThis guide is the complete migration path: every LangChainGo concept mapped to its Genkit equivalent, with before and after code for each one. Follow it top to bottom and you can move a real service in an afternoon.\n\nIf you want the case for *why* rather than the *how*, read the companion article: [LangChainGo vs Genkit Go: where Genkit shines](https://xavidop.me/genkit/2026-09-11-langchaingo-vs-genkit-go/).\n\nThree things in LangChainGo are broken today, and none of them will be fixed, because nobody is upstream to fix them.\n\n**1. The default model no longer exists.** `DefaultOptions()` in `llms/googleai/option.go` hardcodes `gemini-2.0-flash`, which Google retired. A fresh `googleai.New(ctx)` with no options fails on the first call:\n\n```\ngoogleapi: Error 404: This model models/gemini-2.0-flash is no longer available.\nPlease update your code to use models/gemini-3.6-flash for the latest features\nand improvements.\n```\n\n**2. The Google provider runs on an end-of-life SDK.** `llms/googleai` imports `github.com/google/generative-ai-go`, which Google replaced with `google.golang.org/genai`. Its README is explicit:\n\n**End-of-Life Date:** All support for this repository (including bug fixes) will permanently end on **November 30, 2025**.\n\n**3. Streaming is broken on every current Gemini model.** Because of that dead SDK, any streaming call through the Google provider fails:\n\n```\nerror in stream mode: invalid character ']' looking for beginning of value\n```\n\nThis fails on `gemini-2.5-flash`, `gemini-3-flash-preview`, `gemini-3.5-flash` and `gemini-3.6-flash`. Non-streaming calls still work, and the OpenAI provider streams fine, so this is specific to Google on the legacy SDK.\n\nThis one catches people out, so it is worth knowing before you debug it the hard way.\n\nLangChainGo turns on streaming automatically whenever you attach a callbacks handler. From `chains/options.go`:\n\n```\nif opts.StreamingFunc == nil && opts.CallbackHandler != nil {\n    opts.StreamingFunc = func(ctx context.Context, chunk []byte) error {\n        opts.CallbackHandler.HandleStreamingFunc(ctx, chunk)\n        return nil\n    }\n}\n```\n\n`agents/mrkl.go` does the same thing. Since streaming is broken, attaching the only supported observability hook is also what breaks your agent. Same agent, same tool, one line different:\n\n```\nWITHOUT callbacks handler: err=<nil> out=\"The current year is 2026.\"\nWITH    callbacks handler: err=error in stream mode: invalid character ']' looking for beginning of value\n```\n\nOn LangChainGo with Gemini today, you can have working agents or you can have instrumentation. Not both.\n\n```\nnpm install -g genkit\n```\n\nAdd Genkit to your module:\n\n```\ngo get github.com/firebase/genkit/go@latest\n```\n\nThis is the whole translation table. Keep it open while you work.\n\n| LangChainGo | Genkit Go | What changes | \n|---|---|---|\n| `llms.Model` + provider`New()` | plugin + `genkit.Init` | Models become string references like `googleai/gemini-3.5-flash` | \n| `llms.GenerateFromSinglePrompt` | `genkit.GenerateText` | Direct swap | \n| `prompts.PromptTemplate` | `ai.WithPrompt` or a`.prompt` file | Dotprompt files keep prompts outside the binary | \n| `chains.LLMChain` +`chains.Run` | `genkit.DefineFlow` | A typed Go function instead of a `map[string]any` | \n| `chains.NewSequentialChain` | ordinary Go code | Just call the next function | \n| `outputparser.NewDefined[T]` | `genkit.GenerateData[T]` | Real schema instead of prompt-and-parse | \n| `tools.Tool` interface | `genkit.DefineTool[In, Out]` | Typed arguments with a JSON schema | \n| `agents.NewOneShotAgent` (ReAct) | `ai.WithTools` , or`genkitx.DefineAgent` | Native function calling instead of text parsing. Genkit also has a real [agent primitive](https://genkit.dev/docs/go/agents/define/) with sessions | \n| `memory.NewConversationBuffer` | `resp.History()` , or an agent session store | Explicit history, or `aix.WithSessionStore` if you want the buffer back | \n| `embeddings` +`vectorstores` | `localvec` or a vector store plugin | Genkit has a local store, LangChainGo does not | \n| `callbacks.Handler` | built-in tracing | Delete the handler entirely | \n| nothing | `middleware.Retry` ,`middleware.Fallback` | Retry and fallback stop being your code | \n\nIn LangChainGo, each provider is a concrete struct you construct and hold:\n\n```\nllm, err := googleai.New(ctx,\n    googleai.WithDefaultModel(\"gemini-3.5-flash\"),\n    googleai.WithDefaultEmbeddingModel(\"gemini-embedding-001\"))\nif err != nil {\n    log.Fatal(err)\n}\ndefer llm.Close()\n```\n\nIn Genkit, plugins register with one runtime object and models become string references:\n\n```\ng := genkit.Init(ctx,\n    genkit.WithPlugins(&googlegenai.GoogleAI{}),\n    genkit.WithDefaultModel(\"googleai/gemini-3.5-flash\"))\n```\n\nThat difference matters more than it looks. Registering a second vendor is one more plugin:\n\n```\ng := genkit.Init(ctx, genkit.WithPlugins(\n    &googlegenai.GoogleAI{},\n    &openai.OpenAI{},\n))\n```\n\nNow both vendors live in the same registry and a model is just a name, which is what makes swapping and falling back a configuration change later on.\n\nA plain text generation is close to a direct swap.\n\n**Before:**\n\n```\nout, err := llms.GenerateFromSinglePrompt(ctx, llm,\n    \"In one sentence: what is Go's error handling philosophy?\")\n```\n\n**After:**\n\n```\nout, err := genkit.GenerateText(ctx, g,\n    ai.WithPrompt(\"In one sentence: what is Go's error handling philosophy?\"))\n```\n\nIf you need the full response rather than just the text, use `genkit.Generate`, which gives you `resp.Text()`, `resp.History()` and usage data.\n\nA LangChainGo chain wraps a prompt template and takes a `map[string]any`:\n\n```\nprompt := prompts.NewPromptTemplate(\n    \"Triage this support ticket.\\n{{.format}}\\n\\nTicket: {{.ticket}}\",\n    []string{\"format\", \"ticket\"},\n)\nchain := chains.NewLLMChain(llm, prompt)\nraw, err := chains.Predict(ctx, chain, map[string]any{\n    \"format\": parser.GetFormatInstructions(),\n    \"ticket\": ticket,\n})\n```\n\nA Genkit flow is a typed Go function. Your input and output types are the contract and the compiler enforces them:\n\n```\ntriage := genkit.DefineFlow(g, \"triage\", func(ctx context.Context, ticket string) (*Triage, error) {\n    out, _, err := genkit.GenerateData[Triage](ctx, g,\n        ai.WithPrompt(\"Triage this support ticket: %s\", ticket))\n    return out, err\n})\n\nout, err := triage.Run(ctx, \"I was charged twice for order A-1029.\")\n```\n\nThis step deletes the most code, because most of what a chain does is pass a map around. Go does not need that.\n\n**Sequential chains have no equivalent, and do not need one.** `chains.NewSequentialChain` becomes calling the next function:\n\n```\nsummary, err := summarize.Run(ctx, doc)\nif err != nil {\n    return nil, err\n}\nreturn classify.Run(ctx, summary)\n```\n\nPrioritise this step. In LangChainGo, structured output is prompt engineering. In Genkit it is a schema.\n\n**Before:** generate format instructions from your struct, paste them into the prompt, parse the text back.\n\n```\ntype Ticket struct {\n    Category string `json:\"category\" describe:\"one of: billing, bug, feature, other\"`\n    Severity int    `json:\"severity\" describe:\"1 (low) to 5 (critical)\"`\n    Summary  string `json:\"summary\" describe:\"one sentence\"`\n}\n\nparser, err := outputparser.NewDefined(Ticket{})\n// ...put parser.GetFormatInstructions() into the prompt...\nout, err := parser.Parse(raw)\n```\n\n**After:** one call, with the schema sent to the model as a real response schema.\n\n```\ntype Ticket struct {\n    Category string `json:\"category\" jsonschema:\"enum=billing,enum=bug,enum=feature,enum=other\"`\n    Severity int    `json:\"severity\" jsonschema_description:\"1 (low) to 5 (critical)\"`\n    Summary  string `json:\"summary\" jsonschema_description:\"one sentence\"`\n}\n\nout, _, err := genkit.GenerateData[Ticket](ctx, g,\n    ai.WithPrompt(\"Triage this support ticket: %s\", ticket))\n```\n\nNote the tag change: `describe:` becomes `jsonschema_description:`, and enums move into a `jsonschema:` tag where the model actually enforces them.\n\nThe LangChainGo tool interface is `Call(ctx context.Context, input string) (string, error)`. One string in, one string out, no schema. A tool that needs typed arguments has to describe its schema in prose and unmarshal by hand:\n\n```\nfunc (RefundTool) Name() string { return \"issue_refund\" }\n\nfunc (RefundTool) Description() string {\n    return `Issues a refund. Input MUST be a JSON object string with exactly these keys:\n{\"order_id\": string, \"amount_cents\": integer}. Example: {\"order_id\":\"A-1\",\"amount_cents\":2500}`\n}\n\nfunc (RefundTool) Call(ctx context.Context, input string) (string, error) {\n    input = strings.TrimSpace(input)\n    input = strings.TrimPrefix(input, \"``` json\")\n    input = strings.TrimPrefix(input, \"```\")\n    input = strings.TrimSuffix(input, \"```\")\n    var a RefundArgs\n    if err := json.Unmarshal([]byte(strings.TrimSpace(input)), &a); err != nil {\n        return fmt.Sprintf(\"error: input was not valid JSON (%v). Got: %s\", err, input), nil\n    }\n    return fmt.Sprintf(\"refunded %d cents for order %s\", a.AmountCents, a.OrderID), nil\n}\n```\n\nIn Genkit the tool function is generic over its argument type and the schema comes from the struct:\n\n```\ntype RefundArgs struct {\n    OrderID     string `json:\"order_id\"`\n    AmountCents int    `json:\"amount_cents\"`\n}\n\nrefund := genkit.DefineTool(g, \"issue_refund\", \"Issues a refund for an order.\",\n    func(ctx *ai.ToolContext, a RefundArgs) (string, error) {\n        return fmt.Sprintf(\"refunded %d cents for order %s\", a.AmountCents, a.OrderID), nil\n    })\n```\n\nDelete the fence stripping, the unmarshalling and the prose schema. The model receives a real JSON schema and the arguments arrive typed.\n\nA LangChainGo ReAct agent drives tools by parsing the model's text output for `Action:` and `Action Input:` lines, wrapped in an executor with an iteration cap:\n\n```\nagent := agents.NewOneShotAgent(llm, []tools.Tool{RefundTool{}}, agents.WithMaxIterations(6))\nout, err := chains.Run(ctx, agents.NewExecutor(agent),\n    \"Refund order A-1029 for 25 dollars and 50 cents.\")\n```\n\nIn Genkit, native function calling means a one-shot agent is just a generate call with tools attached:\n\n```\nout, err := genkit.GenerateText(ctx, g,\n    ai.WithPrompt(\"Refund order A-1029 for 25 dollars and 50 cents.\"),\n    ai.WithTools(refund))\n```\n\nNo executor, no iteration cap, no text parsing. Genkit runs the tool loop for you.\n\nThe generate call above replaces a `OneShotAgent`. For anything that has to hold a conversation, Genkit Go ships an actual [agent primitive](https://genkit.dev/docs/go/agents/define/) with sessions and persisted state, which LangChainGo has no equivalent for:\n\n```\nimport (\n    aix \"github.com/firebase/genkit/go/ai/exp\"\n    \"github.com/firebase/genkit/go/ai/exp/localstore\"\n    genkitx \"github.com/firebase/genkit/go/genkit/exp\"\n)\n\ng := genkit.Init(ctx,\n    genkit.WithPlugins(&googlegenai.GoogleAI{}),\n    genkit.WithDefaultModel(\"googleai/gemini-2.5-flash\"),\n    genkit.WithExperimental())\n\nstore := localstore.NewInMemorySessionStore[State]()\n\nagent := genkitx.DefineAgent(g, \"taskAgent\",\n    aix.InlinePrompt{\n        ai.WithSystem(\"Manage a task list. Use tools when changing tasks.\"),\n        ai.WithTools(addTask),\n    },\n    aix.WithSessionStore(store),\n    aix.WithDescription[State](\"Task management assistant\"),\n)\n```\n\nThree constructors are available: `DefineAgent` for an inline prompt, `DefinePromptAgent` to wrap a prompt already in the registry, and `DefineCustomAgent` to replace the prompt loop with your own code. Options cover a session store, `WithStateTransform` to reshape state on the way out to a client, and `WithStreamTransform` for chunks. You drive one with `Run`, `RunText`, or `Connect` for a streaming connection.\n\n**These live in the `exp` packages.** Agents are experimental in Genkit Go v1.13.1, and calling `genkitx.DefineAgent` without `genkit.WithExperimental()` on `genkit.Init` panics with a message telling you so. The API may change between minor releases, so weigh that before putting one in production.\n\nCheck this step before planning your timeline, because it has an infrastructure consequence.\n\nLangChainGo ships fifteen vector stores: AlloyDB, Azure AI Search, Bedrock Knowledge Bases, Chroma, CloudSQL, Dolt, MariaDB, Milvus, MongoDB, OpenSearch, pgvector, Pinecone, Qdrant, Redis and Weaviate. **Every one of them needs an external service running.** There is no in-memory or local option, and the pull request adding one has been open since February 2024. If you needed a local store, you wrote it yourself.\n\nGenkit ships `localvec`, a file-backed local store, so the same thing is three calls:\n\n```\nembedder := googlegenai.GoogleAIEmbedder(g, \"gemini-embedding-001\")\nstore, retriever, err := localvec.DefineRetriever(g, \"policy\",\n    localvec.Config{Dir: \"/tmp/gkvec\", Embedder: embedder}, nil)\nif err != nil {\n    return err\n}\n\n// Index your documents once.\nlocalvec.Index(ctx, docs, store)\n\n// Retrieve at request time.\nfound, err := genkit.Retrieve(ctx, g, ai.WithRetriever(retriever), ai.WithTextDocs(ticket))\n```\n\nThen hand the documents straight to the model:\n\n```\nout, _, err := genkit.GenerateData[Triage](ctx, g,\n    ai.WithDocs(found.Documents...),\n    ai.WithPrompt(\"Triage this ticket using the policy documents: %s\", ticket))\n```\n\nIf you were on a hosted store, Genkit has Pinecone, Weaviate, Postgres and AlloyDB plugins.\n\nLangChainGo keeps conversation history in a buffer attached to the chain:\n\n```\nc := chains.NewConversation(llm, memory.NewConversationBuffer())\nchains.Run(ctx, c, \"My name is Xavi and I work in Go.\")\nchains.Run(ctx, c, \"What is my name and what language do I use?\")\n```\n\nGenkit passes history explicitly:\n\n```\nresp, err := genkit.Generate(ctx, g, ai.WithPrompt(\"My name is Xavi and I work in Go.\"))\nif err != nil {\n    return err\n}\n\nresp, err = genkit.Generate(ctx, g,\n    ai.WithMessages(resp.History()...),\n    ai.WithPrompt(\"What is my name and what language do I use?\"))\n```\n\nFor a plain generate call this is the one mapping where LangChainGo is less typing. The trade is that Genkit has no hidden buffer to keep in sync, which is what you want the moment your conversation state lives in Redis or Postgres and your handlers are stateless.\n\nIf you want the buffer back, that is what an agent's session store is for. Give the agent a store and pass a session id, and the history is the framework's problem again:\n\n```\nstore := localstore.NewInMemorySessionStore[State]()\n\nagent := genkitx.DefineAgent(g, \"taskAgent\",\n    aix.InlinePrompt{\n        ai.WithSystem(\"Manage a task list. Use tools when changing tasks. Be terse.\"),\n        ai.WithTools(addTask),\n    },\n    aix.WithSessionStore(store))\n\nfor _, turn := range []string{\n    \"Add 'buy milk' to my list.\",\n    \"What did I just ask you to add?\",\n} {\n    out, err := agent.RunText(ctx, turn, aix.WithSessionID[State](\"demo-session\"))\n    if err != nil {\n        return err\n    }\n    fmt.Printf(\"> %s\\n%s\\n\\n\", turn, out.Message.Text())\n}\n> Add 'buy milk' to my list.\nTask added.\n\n> What did I just ask you to add?\nYou asked me to add 'buy milk' to your list.\n```\n\nNothing in that loop passes the first turn into the second. The session id does it. `localstore.NewInMemorySessionStore` is the one that ships for local development; the `SessionStore` interface is what you implement to put sessions in Redis or Postgres.\n\nSo the honest version of this row is: Genkit gives you both ends. Explicit `resp.History()` when you want stateless handlers, and a store-backed session when you want the buffer.\n\nThis is the step that usually surprises people with how much it removes.\n\nLangChainGo has no middleware layer. `callbacks.Handler` is seventeen methods that all return nothing:\n\n```\nHandleText(ctx, text)\nHandleLLMGenerateContentStart(ctx, ms)\nHandleLLMGenerateContentEnd(ctx, res)\nHandleToolStart(ctx, input)\n// ...and thirteen more\n```\n\nYou can watch a call. You cannot wrap it, retry it, rewrite its request or substitute a model, because there is no `next()`. There is no retry, no fallback and no OpenTelemetry anywhere in the library, so if your service has those, you wrote them.\n\nIn Genkit they are configuration:\n\n```\nout, _, err := genkit.GenerateData[Triage](ctx, g,\n    ai.WithModel(googlegenai.GoogleAIModelRef(\"gemini-3.5-flash\", nil)),\n    ai.WithDocs(found.Documents...),\n    ai.WithTools(refund),\n    ai.WithPrompt(\"Triage this ticket and act on it: %s\", ticket),\n    ai.WithUse(\n        &middleware.Retry{MaxRetries: 2},\n        &middleware.Fallback{Models: []ai.ModelRef{openai.ModelRef(\"gpt-5-mini\", nil)}},\n    ),\n)\n```\n\nRegister the middleware plugin at init and those two lines are live:\n\n```\ng := genkit.Init(ctx, genkit.WithPlugins(\n    &googlegenai.GoogleAI{}, &openai.OpenAI{}, &middleware.Middleware{},\n))\n```\n\nPoint the primary at a dead model and the request heals onto the other vendor on its own:\n\n```\nWARN model call failed, falling back model=openai/gpt-5-mini error=\"Error 404 ... NOT_FOUND\"\n{Category:billing Severity:2 PolicyQuote:Duplicate charges must be refunded in full within 5\nbusiness days, no manager approval required. Action:Issued full refund of $25.50 (2550 cents)\nfor the duplicate charge on order A-1029.}\n```\n\nGemini went down, OpenAI picked it up, the tool still ran, and the typed struct came back intact.\n\n**Delete your callbacks handler too.** Genkit traces flows, generate calls, tool calls and retrieval automatically. Run the Developer UI to see them:\n\n```\ngenkit start -- go run .\n```\n\nFinally, expose your flows over HTTP. Where LangChainGo left you writing `net/http` handlers around chains, Genkit turns a flow into an endpoint:\n\n```\nmux := http.NewServeMux()\nmux.HandleFunc(\"POST /triage\", genkit.Handler(triage))\nlog.Fatal(server.Start(ctx, \"127.0.0.1:3400\", mux))\n```\n\nThe flow's input and output types define the request and response shapes, so the whole path is typed end to end.\n\nThe migration is not free. Check these three things first:\n\n**Provider and vector store catalog.** LangChainGo has seventeen model providers and fifteen vector stores. Genkit Go covers Google AI, Vertex, OpenAI, Anthropic, Ollama and the OpenAI-compatible vendors, plus Pinecone, Weaviate, Postgres and AlloyDB. If you are on Dolt, MariaDB or Bedrock Knowledge Bases as a vector store, confirm you have a path.\n\n**Conversation buffers.** There is no drop-in `ConversationBuffer`. You pass history explicitly, as in Step 8.\n\n**Specialised chains.** Helpers like `chains.NewSQLDatabaseChain` and the map-reduce summarisation chains have no one-to-one equivalent. In Genkit they are Go functions you write, which is usually shorter than the chain was, but it is work.\n\nThe migration is smaller than it looks, because most of a LangChainGo program is plumbing that Go does not need: maps passed between chains, format instructions pasted into prompts, JSON unmarshalled out of tool strings, and a resilience layer you wrote yourself. Typed flows, typed tools, schema-backed output and middleware delete all four.\n\nThe reason to start now is not ergonomics. It is that the library your service depends on has a retired default model, an end-of-life SDK underneath its Google provider, broken streaming, and an observability hook that breaks the agents it is supposed to observe. Those are not problems you can wait out.\n\nFor the side-by-side numbers and the full picture, read the companion: [LangChainGo vs Genkit Go: where Genkit shines](https://xavidop.me/genkit/2026-09-11-langchaingo-vs-genkit-go/).\n\nFurther reading:", "url": "https://wpnews.pro/news/how-to-migrate-from-langchaingo-to-genkit-go", "canonical_source": "https://dev.to/xavidop/how-to-migrate-from-langchaingo-to-genkit-go-3a44", "published_at": "2026-09-29 13:40:34+00:00", "updated_at": "2026-09-29 13:46:47.796452+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "large-language-models", "ai-agents", "generative-ai"], "entities": ["LangChainGo", "Genkit Go", "Google", "Gemini", "github.com/google/generative-ai-go", "google.golang.org/genai", "gemini-2.0-flash", "Firebase"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-to-migrate-from-langchaingo-to-genkit-go", "markdown": "https://wpnews.pro/news/how-to-migrate-from-langchaingo-to-genkit-go.md", "text": "https://wpnews.pro/news/how-to-migrate-from-langchaingo-to-genkit-go.txt", "jsonld": "https://wpnews.pro/news/how-to-migrate-from-langchaingo-to-genkit-go.jsonld"}}